Clean Architecture — alapok, Entities, Use Cases és Gateways rétegek

Szerző: IT Sectr Megjelenés: 2026-02-17 Olvasási idő: 10 perc

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 — három réteg: Domain (üzleti logika), Data (adatok), Presentation (UI) Dependency Rule-lal
  • Dependency Rule — a függőségek befelé irányulnak, a Domain nem tud a Data-ról és Presentation-ről
  • Use Cases (Interactors) — üzleti logikai forgatókönyvek, minden Use Case — egy osztály egy metódussal
  • Repository Interface — adatabsztrakció a Domain-ben, implementáció — a Data rétegben
  • Tesztelhetőség — Domain és Use Cases egységtesztekkel tesztelhetők Android SDK és iOS UIKit nélkül

Clean Architecture — a többrétegű architektúra alapjai

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étegTartalmazFüggőségek
DomainEntities, Use Cases, Repository InterfacesNincs (tiszta Kotlin/Swift)
DataRepositoryImpl, DataSources (API, DB, Cache)Domain, Retrofit, Room, Ktor
PresentationViewModels, Views, ComposablesDomain, 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 Layer: Entities, Use Cases és Repository Interfaces

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.

swift
// 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: Repository Implementation és DataSources

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.

kotlin
// 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: ViewModels és Views

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.

kotlin
// 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 iOS-en és Androidon: kódpéldák

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.

swift
// 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

Hány réteg legyen a Clean Architecture-ben?

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.

Növeli-e a Clean Architecture a kód mennyiségét?

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.

Kombinálható a Clean Architecture az MVI-vel?

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.

Kell Use Case minden adatlekéréshez?

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.

Hogyan teszteljük a Clean Architecture-t?

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ó

  • Clean Architecture — három réteg (Domain, Data, Presentation) Dependency Rule-lal befelé
  • Dependency Rule — Domain nem tud Data-ról és Presentation-ről, izoláció interfészekkel
  • Domain — Entities, Use Cases, Repository Interfaces — tiszta Kotlin/Swift keretrendszerek nélkül
  • Data — RepositoryImpl, DataSources (hálózat, adatbázis, gyorsítótár) — Domain interfészek implementációja
  • Presentation — ViewModels, Views — csak megjelenítés, üzleti logika Use Cases-ben
  • Tesztelés — Domain 90–95%-ban fedett egységtesztekkel
  • KMP — Clean Architecture Kotlin Multiplatform-mal 60–80% közös kódot ad iOS + Androidra

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.

Projekt megbeszélése

Olvassa el is