Clean Architecture — Robert Martin (Uncle Bob) által 2012-ben javasolt többrétegű architektúra, amely független rétegekre osztja az alkalmazást: Domain (Entities, Use Cases), Data (Repositories, DataSources) és Presentation (ViewModels, Views). A fő elv — Dependency Rule: a függőségek befelé irányulnak, a külső rétegek a belsőktől függenek, nem fordítva. A Clean Architecture-ot magas üzleti logikai komplexitású projektekhez használják mobilfejlesztésben. Bővebben — a The Clean Architecture könyvben.
Főbb pontok
Clean Architecture — Robert Martin (Uncle Bob) által 2012-ben megfogalmazott architektúra minta. A fő ötlet — az alkalmazás rétegekre osztása szigorú függőségi szabállyal: a rétegen belüli kód nem tud a külső kódról. Külső rétegek (UI, keretrendszerek, adatbázisok) — implementációs részletek. Belső rétegek (üzleti logika, vállalati szabályok) — az alkalmazás lényege.
Clean Architecture rétegek mobilfejlesztésben: 1) Domain — Entities (üzleti objektumok) és Use Cases (használati esetek); 2) Data — RepositoryImpl (repozitóriumok implementációja), DataSources (hálózat, adatbázis, gyorsítótár); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain — a legbelső réteg, függőségek nélkül. Data függ a Domain-től (megvalósítja a repozitórium interfészeket). Presentation függ a Domain-től (meghívja a Use Cases-eket, feliratkozik az eredményre).
| Réteg | Tartalmaz | Függőségek |
|---|---|---|
| Domain | Entities, Use Cases, Repository Interfaces | Nincs (tiszta Kotlin/Swift) |
| Data | RepositoryImpl, DataSources (API, DB, Cache) | Domain, Retrofit, Room, Ktor |
| Presentation | ViewModels, Views, Composables | Domain, Jetpack, SwiftUI |
Dependency Rule — a Clean Architecture egyetlen szigorú szabálya. A forráskód csak a saját rétegében lévő vagy az alatta lévő rétegre (közelebb a központhoz) hivatkozhat. A Presentation importálja a Domain-t. A Domain NEM importálja a Data-t vagy Presentation-t. Ezt a függőségek megfordításával (Dependency Inversion Principle) érik el: a Domain meghatározza a Repository interfészt, a Data implementálja. A Presentation a UseCase absztrakciótól függ, nem a konkrét repozitóriumtól.
Domain — az alkalmazás legstabilabb rétege. Entities — keretrendszerektől független üzleti objektumok: User, Product, Order. Use Cases — osztályok egyetlen invoke metódussal (vagy operator fun invoke Kotlinban), amelyek egy forgatókönyvet valósítanak meg: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Repository Interfaces — adatelérési absztrakciók, a Domain-ben meghatározva, a Data-ban implementálva. A Domain nem tartalmaz Android SDK-t, iOS UIKit-et, Retrofit-ot, Room-ot — csak tiszta Kotlin-t vagy Swift-et.
// Entity — üzleti objektum (Domain)
struct User: Equatable {
let id: Int
let name: String
let email: String
}
// Repository Interface — adatabsztrakció (Domain)
protocol UserRepository {
func getUser(id: Int) async throws -> User
func getUsers() async throws -> [User]
}
// Use Case — egy forgatókönyv (Domain)
final class GetUserUseCase {
private let repository: UserRepository
init(repository: UserRepository) {
self.repository = repository
}
func execute(id: Int) async throws -> User {
return try await repository.getUser(id: id)
}
}
Use Case — «egy metódusú osztály» — nem dogma, hanem gyakorlati ajánlás. Amikor a Use Case bonyolultabbá válik (validáció + naplózás + repozitórium hívás), metódusai jelentés szerint csoportosíthatók: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. Fontos — a Use Case nem tudhatja, honnan jönnek az adatok (hálózat, adatbázis, gyorsítótár) és ki jeleníti meg őket (Compose, SwiftUI). Az IT Sectr-nél minden olyan művelethez külön Use Case-t rendelünk, amely üzleti szabályt, ellenőrzést vagy két forrásból származó adat kombinálását tartalmazza.
A Domain tisztasága a DTO-leképezéssel érhető el a rétegek határain. A Data réteg JSON modelleket (DTO) kap, leképezi azokat Domain Entity-vé. A Presentation Domain Entity-t kap, leképezi ViewModel-lé (DisplayItem). A Domain Entity soha nem tartalmaz Retrofit, Room, Codable annotációkat — ez garantálja, hogy a réteget nem kell módosítani az adatbázis Room-ról Realm-re váltásakor vagy a Retrofit Ktor-ra cserélésekor.
Data Layer — a Domain-ben meghatározott interfészek implementációja. Tartalmazza a RepositoryImpl (UserRepository-t implementáló osztályok) és DataSources (RemoteDataSource — API, LocalDataSource — adatbázis, CacheDataSource — SharedPreferences/NSUserDefaults) elemeket. A Data réteg függ a Domain-től (importálja a repozitórium interfészeket és Entities-eket) és a keretrendszerektől (Retrofit, Room, Ktor, CoreData). A RepositoryImpl elrejti a Domain elől az adatforrást — a Use Case nem tudja, hogy az adatok a hálózatról vagy a gyorsítótárból származnak.
// DTO — modell a hálózathoz (Data)
data class UserDto(
@SerializedName("id") val id: Int,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
@SerializedName("email") val email: String
)
// RepositoryImpl — implementáció (Data)
class UserRepositoryImpl(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun getUser(id: Int): User {
// Megpróbáljuk elővenni a gyorsítótárból
localDataSource.getUser(id)?.let { return it.toDomain() }
// Ha nincs — betöltjük a hálózatról
val dto = remoteDataSource.fetchUser(id)
val user = dto.toDomain()
localDataSource.saveUser(user)
return user
}
override suspend fun getUsers(): List<User> {
return remoteDataSource.fetchAllUsers().map { it.toDomain() }
}
}
// Mapper — DTO ↔ Domain konverzió
fun UserDto.toDomain() = User(
id = id,
name = "$firstName $lastName",
email = email
)
Gyorsítótárazási stratégia a Data Layer-ben: a RepositoryImpl először ellenőrzi a helyi tárolót, adatok hiányában — betölt a hálózatról és helyben elmenti. Ha a hálózat nem elérhető — elavult adatokat ad vissza az isStale jelzéssel. A Domain-ben lévő Use Case nem tud a stratégiáról — a User-t a Repository.getUser(id) segítségével kapja meg. A stratégia megváltoztatása (pl. gyorsítótár érvénytelenítése 15 percenként) nem érinti a Domain-t és a Presentation-t.
Modularitás Androidon — a Kotlin Multiplatform lehetővé teszi a Domain kiszervezését egy külön KMP modulba Android SDK függőségek nélkül. Data — külön modul Domain függőséggel. Presentation — Android modul Domain függőséggel. Gradle függőségek: domain (pure Kotlin), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Ez a modularitás kötelező nagy projekteknél — a CI külön építi a Domain-t, a Domain egységtesztjei nem igényelnek Android emulátort.
Presentation Layer — a Clean Architecture legkülső rétege. Tartalmazza a ViewModels (Android) / ObservableObject (iOS) és Views (Compose/SwiftUI) elemeket. A ViewModel meghívja a Use Case-t, megkapja az eredményt és UI állapottá (State) alakítja. A View feliratkozik az State-re és megjeleníti. A Presentation függ a Domain-től — importálja a Use Cases-eket és Entities-eket. A Presentation nem importálja a Data Layer-t — az adatok a Use Case-en keresztül érkeznek, amely belsőleg Repository-t használ.
ViewModel a Clean Architecture-ben nem tartalmaz üzleti logikát — meghívja a Use Case-t. Ha a Use Case User-t ad vissza, a ViewModel UserDisplayItem-má (name, emailFormatted, avatarUrl) alakítja — egy tiszta prezentációs modellé. A Use Case nem tud a DisplayItem-ről — Entity-t ad vissza. Ez a szétválasztás lehetővé teszi a Use Case UI nélküli és a ViewModel UseCase nélküli (mock segítségével) tesztelését. Az IT Sectr-nél szigorúan betartjuk: Use Case — üzleti logika, ViewModel — csak prezentáció, View — csak megjelenítés.
// Use Case (Domain) — tiszta üzleti logika
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: Int): User {
return repository.getUser(id)
}
}
// ViewModel (Presentation) — csak prezentáció
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _state = MutableStateFlow<UserScreenState>(UserScreenState.Loading)
val state: StateFlow<UserScreenState> = _state.asStateFlow()
fun loadUser(id: Int) {
viewModelScope.launch {
_state.value = UserScreenState.Loading
val user = getUserUseCase(id)
val displayItem = UserDisplayItem(
name = user.name,
email = user.email,
initials = user.name.split(" ").joinToString("") { it.first().toString() }
)
_state.value = UserScreenState.Success(displayItem)
}
}
}
data class UserDisplayItem(
val name: String,
val email: String,
val initials: String
)
sealed interface UserScreenState {
data object Loading : UserScreenState
data class Success(val displayItem: UserDisplayItem) : UserScreenState
data class Error(val message: String) : UserScreenState
}
Navigation a Presentation rétegben — a külső gyűrű része. A Clean Architecture nem ír elő navigációs mechanizmust — lehet NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) vagy Router (VIPER). Fontos: a navigációs döntést a Presentation hozza, de a navigáció nem hatolhat be a Use Case-be. A Use Case visszaadja az eredményt, a ViewModel dönti el, melyik képernyőre lépjen. A Clean Architecture-ben a navigáció egy részlet, amely a Domain megváltoztatása nélkül cserélhető.
Clean Architecture Androidon Gradle modulokon keresztül valósul meg: domain (pure Kotlin), data (domain + Retrofit + Room), presentation (domain + Compose). Mappaszerkezet: domain/user/User.kt, GetUserUseCase.kt, UserRepository.kt; data/remote/UserRemoteDataSource.kt, local/UserDao.kt, repository/UserRepositoryImpl.kt; presentation/ui/user/UserViewModel.kt, UserScreen.kt. A DI (Hilt) összeköti a rétegeket: a UserRepositoryImpl a domain modulban lévő UserRepository interfészhez van kötve.
Clean Architecture iOS-en SPM-et vagy Xcode csoportokat használ külön modulok nélkül (az Xcode korlátozásai miatt). Domain — mappa fájlokkal, amelyek nem importálnak UIKit-et vagy SwiftUI-t. Data — mappa APIClient, CoreDataStack, RepositoryImpl fájlokkal. Presentation — mappa ViewModels és SwiftUI Views fájlokkal. DI konstruktoron vagy assembly-n keresztül az App-ban. A fő hívás — async/await a UseCase.execute()-en keresztül MainActor ellenőrzéssel a UI frissítésekhez.
// Data Layer: Remote DataSource (iOS)
final class UserRemoteDataSource {
private let apiClient: APIClient
func fetchUser(id: Int) async throws -> UserDTO {
return try await apiClient.get("/users/\(id)")
}
}
// Repository Implementation (Data)
final class UserRepositoryImpl: UserRepository {
private let remote: UserRemoteDataSource
private let local: UserLocalDataSource
func getUser(id: Int) async throws -> User {
if let cached = try await local.getUser(id) {
return cached
}
let dto = try await remote.fetchUser(id)
let user = dto.toDomain()
try await local.saveUser(user)
return user
}
}
// Presentation: ViewModel + SwiftUI View
@MainActor
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserScreenState = .loading
private let getUserUseCase: GetUserUseCase
func loadUser(id: Int) {
Task {
state = .loading
if let user = try? await getUserUseCase.execute(id: id) {
state = .success(user)
} else {
state = .error("Failed to load")
}
}
}
}
Clean Architecture az IT Sectr projektjeiben — a szabványunk a 30 napos vagy hosszabb projektekhez. 2022 óta háromrétegű architektúrát használunk Kotlin Multiplatform-mal Android/iOS rendszerekhez. Domain — közös KMP modul, Data — platformmodulok (Retrofit Androidon, URLSession iOS-en), Presentation — natív UI. Ez 60–80% közös üzleti logikai kódot biztosít iOS és Android között, 30–40%-kal csökkentve a fejlesztési időt két külön implementációhoz képest.
Gyakran Ismételt Kérdések
Minimum három: Domain, Data, Presentation. Nagy projektekhez hozzáadják a Framework (Android SDK/iOS UIKit függőségek) és Device (GPS, kamera, érzékelők) rétegeket. A rétegek száma — nem szigorú szabály, hanem kényelmi kérdés. A lényeg a Dependency Rule betartása: a függőségek befelé, a Domain felé irányuljanak. Lehet hárommal kezdeni és rétegeket hozzáadni a projekt növekedésével.
Igen — 30–50%-kal az MVVM-hez képest a repozitórium interfészek, Use Cases és mapperek elkülönítése miatt. Egyszerű CRUD alkalmazáshoz ez túlzó. A Clean Architecture összetett üzleti logikájú projekteknél indokolt, ahol a tesztelhetőség és rétegizoláció fontosabb a fejlesztési sebességnél. MVP-hez vagy prototípushoz használj MVVM-et — a Clean Architecture lelassítja az indulást.
Igen, ez elterjedt gyakorlat. A Use Cases a Domain-ben marad, a Presentation pedig az MVI ciklust (Intent → Reducer → State) használja. Data Layer — ugyanaz, Domain — ugyanaz. Az MVI a Presentation-ben kiszámítható képernyőállapotot ad, a Clean Architecture — az üzleti logika izolációját. Ezt a kombinációt nagy, több tucat fejlesztős projektekben használják.
Use Case akkor kell, ha a művelet üzleti szabályt tartalmaz: validáció, adatok kombinálása két forrásból, számítás, naplózás, hozzáférési jogosultságok ellenőrzése. Egy egyszerű getUser(id) lekérés további logika nélkül közvetlenül is meghívhatja a Repository-t a ViewModel-ből. Az architektúra egységessége érdekében azonban sok csapat minden publikus Repository metódushoz készít Use Case-t — ez 5–10% kódot ad hozzá, de egyszerűsíti az olvashatóságot.
Domain: Use Cases egységtesztelése mock Repository-jal — tiszta Kotlin/Swift Android SDK nélkül. Data: RepositoryImpl integrációs tesztelése mock/fake DataSource-szal. Presentation: ViewModel tesztelése mock UseCase-szal. A Dependency Rule-nak köszönhetően minden réteg izoláltan tesztelhető. Az IT Sectr-nél a Domain lefedettsége eléri a 95%-ot, a Data-é 70–80%-ot, a Presentation-é 60–70%-ot.
Összefoglaló
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is