Clean Architecture — podstawy, warstwy Entities, Use Cases i Gateways

Autor: IT Sectr Opublikowano: 2026-02-17 Czas czytania: 10 min

Clean Architecture — wielowarstwowa architektura zaproponowana przez Roberta Martina (Uncle Bob) w 2012 roku, dzieląca aplikację na niezależne warstwy: Domain (Entities, Use Cases), Data (Repositories, DataSources) i Presentation (ViewModels, Views). Główna zasada — Dependency Rule: zależności skierowane do wewnątrz, zewnętrzne warstwy zależą od wewnętrznych, ale nie odwrotnie. Clean Architecture jest stosowana w programowaniu mobilnym dla projektów o wysokiej złożoności logiki biznesowej. Więcej — w książce The Clean Architecture.

Najważniejsze

  • Clean Architecture — trzy warstwy: Domain (logika biznesowa), Data (dane), Presentation (UI) z Dependency Rule
  • Dependency Rule — zależności skierowane do wewnątrz, Domain nie wie o Data i Presentation
  • Use Cases (Interactors) — scenariusze logiki biznesowej, każdy Use Case — jedna klasa z jedną metodą
  • Repository Interface — abstrakcja danych w Domain, implementacja — w warstwie Data
  • Testowalność — Domain i Use Cases są testowane testami jednostkowymi bez Android SDK i iOS UIKit

Clean Architecture — podstawy wielowarstwowej architektury

Clean Architecture — wzorzec architektoniczny sformułowany przez Roberta Martina (Uncle Bob) w 2012 roku. Główna idea — podział aplikacji na warstwy ze ścisłą regułą zależności: kod wewnątrz warstwy nie wie o kodzie na zewnątrz. Zewnętrzne warstwy (UI, frameworki, bazy danych) — szczegóły implementacji. Wewnętrzne warstwy (logika biznesowa, reguły przedsiębiorstwa) — istota aplikacji.

Warstwy Clean Architecture w programowaniu mobilnym: 1) Domain — Entities (obiekty biznesowe) i Use Cases (przypadki użycia); 2) Data — RepositoryImpl (implementacja repozytoriów), DataSources (sieć, baza danych, pamięć podręczna); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain — najbardziej wewnętrzna warstwa, nieposiadająca zależności. Data zależy od Domain (implementuje interfejsy repozytoriów). Presentation zależy od Domain (wywołuje Use Cases, subskrybuje wynik).

WarstwaZawieraZależności
DomainEntities, Use Cases, Repository InterfacesBrak (czysty Kotlin/Swift)
DataRepositoryImpl, DataSources (API, DB, Cache)Domain, Retrofit, Room, Ktor
PresentationViewModels, Views, ComposablesDomain, Jetpack, SwiftUI

Dependency Rule — jedyna ścisła zasada Clean Architecture. Kod źródłowy może odwoływać się tylko do warstwy wewnątrz siebie lub do warstwy poniżej (bliżej centrum). Presentation importuje Domain. Domain NIE importuje Data ani Presentation. Osiąga się to przez odwrócenie zależności (Dependency Inversion Principle): Domain definiuje interfejs Repository, Data go implementuje. Presentation zależy od abstrakcji UseCase, a nie od konkretnego repozytorium.

Domain Layer: Entities, Use Cases i Repository Interfaces

Domain — najbardziej stabilna warstwa aplikacji. Entities — obiekty biznesowe, niezależne od frameworków: User, Product, Order. Use Cases — klasy z jedną metodą invoke (lub operator fun invoke w Kotlin), implementujące jeden scenariusz: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Repository Interfaces — abstrakcje dostępu do danych, definiowane w Domain, implementowane w Data. Domain nie zawiera Android SDK, iOS UIKit, Retrofit, Room — tylko czysty Kotlin lub Swift.

swift
// Entity — obiekt biznesowy (Domain)
struct User: Equatable {
    let id: Int
    let name: String
    let email: String
}

// Repository Interface — abstrakcja danych (Domain)
protocol UserRepository {
    func getUser(id: Int) async throws -> User
    func getUsers() async throws -> [User]
}

// Use Case — jeden scenariusz (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 — «klasa z jedną metodą» — nie dogmat, a praktyczne zalecenie. Gdy Use Case staje się bardziej złożony (walidacja + logowanie + wywołanie repozytorium), jego metody grupuje się według znaczenia: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. Najważniejsze — Use Case nie powinien wiedzieć, skąd pochodzą dane (sieć, baza danych, pamięć podręczna) i kto je wyświetla (Compose, SwiftUI). W IT Sectr wydzielamy Use Case dla każdej operacji, która ma regułę biznesową, walidację lub łączenie danych z dwóch źródeł.

Czystość Domain osiąga się przez mapowanie DTO na granicach warstw. Warstwa Data otrzymuje modele JSON (DTO), mapuje je na Entity Domain. Presentation otrzymuje Entity Domain, mapuje na ViewModel (DisplayItem). Domain Entity nigdy nie zawiera adnotacji Retrofit, Room, Codable — gwarantuje to, że warstwa nie będzie wymagać zmiany przy przejściu z bazy Room na Realm lub przy zamianie Retrofit na Ktor.

Data Layer: Repository Implementation i DataSources

Data Layer — implementacja interfejsów zdefiniowanych w Domain. Zawiera RepositoryImpl (klasy implementujące UserRepository) i DataSources (RemoteDataSource — API, LocalDataSource — baza danych, CacheDataSource — SharedPreferences/NSUserDefaults). Warstwa Data zależy od Domain (importuje interfejsy repozytoriów i Entities) oraz od frameworków (Retrofit, Room, Ktor, CoreData). RepositoryImpl ukrywa przed Domain źródło danych — Use Case nie wie, czy dane pochodzą z sieci czy pamięci podręcznej.

kotlin
// DTO — model dla sieci (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 — implementacja (Data)
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {

    override suspend fun getUser(id: Int): User {
        // Próbujemy pobrać z pamięci podręcznej
        localDataSource.getUser(id)?.let { return it.toDomain() }
        // Jeśli nie ma — ładujemy z sieci
        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 — konwersja DTO ↔ Domain
fun UserDto.toDomain() = User(
    id = id,
    name = "$firstName $lastName",
    email = email
)

Strategia buforowania w Data Layer: RepositoryImpl najpierw sprawdza lokalne przechowywanie, w przypadku braku danych — ładuje z sieci i zapisuje lokalnie. Jeśli sieć jest niedostępna — zwraca nieaktualne dane z oznaczeniem isStale. Use Case w Domain nie wie o strategii — otrzymuje User przez Repository.getUser(id). Zmiana strategii (np. unieważnianie pamięci podręcznej co 15 minut) nie wpływa na Domain i Presentation.

Modułowość w Android — Kotlin Multiplatform pozwala wydzielić Domain do osobnego modułu KMP bez zależności od Android SDK. Data — osobny moduł z zależnością od Domain. Presentation — moduł Android z zależnością od Domain. Zależności Gradle: domain (pure Kotlin), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Taka modułowość jest obowiązkowa dla dużych projektów — CI buduje Domain osobno, testy jednostkowe Domain nie wymagają emulatora Androida.

Presentation Layer: ViewModels i Views

Presentation Layer — najbardziej zewnętrzna warstwa Clean Architecture. Zawiera ViewModels (Android) / ObservableObject (iOS) i Views (Compose/SwiftUI). ViewModel wywołuje Use Case, otrzymuje wynik i przekształca go w stan UI (State). View subskrybuje State i wyświetla. Presentation zależy od Domain — importuje Use Cases i Entities. Presentation nie importuje Data Layer — dane przychodzą przez Use Case, który wewnętrznie używa Repository.

ViewModel w Clean Architecture nie zawiera logiki biznesowej — wywołuje Use Case. Jeśli Use Case zwraca User, ViewModel przekształca go w UserDisplayItem (name, emailFormatted, avatarUrl) — czysto prezentacyjny model. Use Case nie wie o DisplayItem — zwraca Entity. To rozdzielenie pozwala testować Use Case bez UI i ViewModel bez UseCase (przez mock). W IT Sectr ściśle przestrzegamy: Use Case — logika biznesowa, ViewModel — tylko prezentacja, View — tylko wyświetlanie.

kotlin
// Use Case (Domain) — czysta logika biznesowa
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(id: Int): User {
        return repository.getUser(id)
    }
}

// ViewModel (Presentation) — tylko prezentacja
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 w warstwie Presentation — również część zewnętrznego pierścienia. Clean Architecture nie narzuca mechanizmu nawigacji — może to być NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) lub Router (VIPER). Ważne: decyzję o nawigacji podejmuje Presentation, ale nawigacja nie powinna przenikać do Use Case. Use Case zwraca wynik, ViewModel decyduje, na który ekran przejść. W Clean Architecture nawigacja jest szczegółem, który można wymienić bez zmiany Domain.

Clean Architecture na iOS i Android: przykłady kodu

Clean Architecture na Android jest realizowana przez moduły Gradle: domain (pure Kotlin), data (domain + Retrofit + Room), presentation (domain + Compose). Struktura folderów: 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. DI (Hilt) łączy warstwy: UserRepositoryImpl jest przypisany do interfejsu UserRepository w module domain.

Clean Architecture na iOS używa SPM lub grup Xcode bez osobnych modułów (z powodu ograniczeń Xcode). Domain — folder z plikami nieimportującymi UIKit ani SwiftUI. Data — folder z APIClient, CoreDataStack, RepositoryImpl. Presentation — folder z ViewModels i SwiftUI Views. DI przez konstruktor lub assembly w App. Główne wywołanie — async/await przez UseCase.execute() z sprawdzeniem MainActor dla aktualizacji UI.

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 w projektach IT Sectr — nasz standard dla projektów od 30 dni. Używamy trójwarstwowej architektury z Kotlin Multiplatform dla Android/iOS od 2022 roku. Domain — wspólny moduł KMP, Data — moduły platformowe (Retrofit na Android, URLSession na iOS), Presentation — natywne UI. Daje to 60–80% wspólnego kodu logiki biznesowej między iOS a Android, skracając czas rozwoju o 30–40% w porównaniu z dwiema osobnymi implementacjami.

Często zadawane pytania

Ile warstw powinno być w Clean Architecture?

Minimalnie trzy: Domain, Data, Presentation. Dla dużych projektów dodaje się Framework (zależności Android SDK/iOS UIKit) i Device (GPS, kamera, czujniki). Liczba warstw — nie sztywna reguła, a kwestia wygody. Najważniejsze — przestrzegać Dependency Rule: zależności skierowane do wewnątrz, ku Domain. Można zacząć od trzech i dodawać warstwy w miarę wzrostu projektu.

Czy Clean Architecture zwiększa ilość kodu?

Tak — o 30–50% w porównaniu z MVVM z powodu wydzielenia interfejsów repozytoriów, Use Cases i mapperów. Dla prostej aplikacji CRUD to nadmiarowe. Clean Architecture jest uzasadniona dla projektów ze złożoną logiką biznesową, gdzie testowalność i izolacja warstw są ważniejsze niż szybkość rozwoju. Dla MVP lub prototypu używaj MVVM — Clean Architecture spowolni uruchomienie.

Czy można łączyć Clean Architecture z MVI?

Tak, to powszechna praktyka. Use Cases pozostają w Domain, a Presentation używa cyklu MVI (Intent → Reducer → State). Data Layer — ten sam, Domain — ten sam. MVI w Presentation daje przewidywalny stan ekranu, Clean Architecture — izolację logiki biznesowej. Taka kombinacja jest używana w dużych projektach z dziesiątkami programistów.

Czy Use Cases są potrzebne dla każdego zapytania do danych?

Use Case jest potrzebny, gdy operacja zawiera regułę biznesową: walidację, łączenie danych z dwóch źródeł, obliczenia, logowanie, sprawdzanie uprawnień. Proste zapytanie getUser(id) bez dodatkowej logiki może wywoływać Repository bezpośrednio z ViewModel. Jednak dla jednolitości architektury wiele zespołów tworzy Use Case dla każdej publicznej metody Repository — dodaje to 5–10% kodu, ale upraszcza czytanie.

Jak testować Clean Architecture?

Domain: testy jednostkowe Use Cases z mock Repository — czysty Kotlin/Swift bez Android SDK. Data: testy integracyjne RepositoryImpl z mock/fake DataSource. Presentation: testy ViewModel z mock UseCase. Dzięki Dependency Rule każda warstwa jest testowana w izolacji. W IT Sectr pokrycie Domain osiąga 95%, Data — 70–80%, Presentation — 60–70%.

Podsumowanie

  • Clean Architecture — trzy warstwy (Domain, Data, Presentation) z Dependency Rule do wewnątrz
  • Dependency Rule — Domain nie wie o Data i Presentation, izolacja przez interfejsy
  • Domain — Entities, Use Cases, Repository Interfaces — czysty Kotlin/Swift bez frameworków
  • Data — RepositoryImpl, DataSources (sieć, baza danych, pamięć podręczna) — implementacja interfejsów Domain
  • Presentation — ViewModels, Views — tylko wyświetlanie, logika biznesowa w Use Cases
  • Testowanie — Domain pokryty testami jednostkowymi w 90–95%
  • KMP — Clean Architecture z Kotlin Multiplatform daje 60–80% wspólnego kodu iOS + Android

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również