Clean Architecture — основи, шари Entities, Use Cases та Gateways

Автор: IT Sectr Опубліковано: 2026-02-17 Час читання: 10 хв

Clean Architecture — багатошарова архітектура, запропонована Робертом Мартіном (Uncle Bob) у 2012 році, яка розділяє застосунок на незалежні шари: Domain (Entities, Use Cases), Data (Repositories, DataSources) і Presentation (ViewModels, Views). Головний принцип — Dependency Rule: залежності спрямовані всередину, зовнішні шари залежать від внутрішніх, але не навпаки. Clean Architecture застосовується в мобільній розробці для проєктів з високою складністю бізнес-логіки. Докладніше — у книзі The Clean Architecture.

Головне

  • Clean Architecture — три шари: Domain (бізнес-логіка), Data (дані), Presentation (UI) з Dependency Rule
  • Dependency Rule — залежності спрямовані всередину, Domain не знає про Data та Presentation
  • Use Cases (Interactors) — сценарії бізнес-логіки, кожен Use Case — один клас з одним методом
  • Repository Interface — абстракція даних у Domain, реалізація — у Data шарі
  • Тестованість — Domain та Use Cases тестуються unit-тестами без Android SDK та iOS UIKit

Clean Architecture — основи багатошарової архітектури

Clean Architecture — архітектурний патерн, сформульований Робертом Мартіном (Uncle Bob) у 2012 році. Основна ідея — розділення застосунку на шари із жорстким правилом залежностей: код всередині шару не знає про код ззовні. Зовнішні шари (UI, фреймворки, БД) — деталі реалізації. Внутрішні шари (бізнес-логіка, правила підприємства) — суть застосунку.

Шари Clean Architecture в мобільній розробці: 1) Domain — Entities (бізнес-об'єкти) та Use Cases (сценарії використання); 2) Data — RepositoryImpl (реалізації репозиторіїв), DataSources (мережа, БД, кеш); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain — найвнутрішніший шар, що не має залежностей. Data залежить від Domain (реалізує інтерфейси репозиторіїв). Presentation залежить від Domain (викликає Use Cases, підписується на результати).

ШарМіститьЗалежності
DomainEntities, Use Cases, Repository InterfacesНемає (чистий Kotlin/Swift)
DataRepositoryImpl, DataSources (API, БД, Кеш)Domain, Retrofit, Room, Ktor
PresentationViewModels, Views, ComposablesDomain, Jetpack, SwiftUI

Dependency Rule — єдине жорстке правило Clean Architecture. Вихідний код може посилатися лише на шар усередині себе або на шар нижче (ближче до центру). Presentation імпортує Domain. Domain НЕ імпортує Data або Presentation. Це досягається через інверсію залежностей (Dependency Inversion Principle): Domain визначає інтерфейс Repository, Data реалізує його. Presentation залежить від абстракції UseCase, а не від конкретного репозиторію.

Domain Layer: Entities, Use Cases та Repository Interfaces

Domain — найстабільніший шар застосунку. Entities — бізнес-об'єкти, незалежні від фреймворків: User, Product, Order. Use Cases — класи з одним методом invoke (або operator fun invoke у Kotlin), що реалізують один сценарій: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Repository Interfaces — абстракції доступу до даних, що визначаються в Domain та реалізуються в Data. Domain не містить Android SDK, iOS UIKit, Retrofit, Room — лише чистий Kotlin або Swift.

swift
// Entity — бізнес-об'єкт (Domain)
struct User: Equatable {
    let id: Int
    let name: String
    let email: String
}

// Repository Interface — абстракція даних (Domain)
protocol UserRepository {
    func getUser(id: Int) async throws -> User
    func getUsers() async throws -> [User]
}

// Use Case — один сценарій (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 — «клас з одним методом» — не догма, а практична рекомендація. Коли Use Case стає складнішим (валідація + логування + виклик репозиторію), його методи групуються за змістом: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. Головне — Use Case не повинен знати, звідки приходять дані (мережа, БД, кеш) і хто їх відображає (Compose, SwiftUI). В IT Sectr ми виділяємо Use Case для кожної операції, яка має бізнес-правило, перевірку або комбінування даних із двох джерел.

Чистота Domain досягається через DTO-маппінг на межі шарів. Data шар отримує JSON-моделі (DTO), маппить їх у Domain Entity. Presentation отримує Domain Entity, маппить у ViewModel (DisplayItem). Domain Entity ніколи не містить анотацій Retrofit, Room, Codable — це гарантує, що шар не доведеться змінювати при зміні БД з Room на Realm або при заміні Retrofit на Ktor.

Data Layer: Repository Implementation та DataSources

Data Layer — реалізація інтерфейсів, визначених у Domain. Містить RepositoryImpl (класи, що реалізують UserRepository) та DataSources (RemoteDataSource — API, LocalDataSource — БД, CacheDataSource — SharedPreferences/NSUserDefaults). Data шар залежить від Domain (імпортує інтерфейси репозиторіїв та Entities) та від фреймворків (Retrofit, Room, Ktor, CoreData). RepositoryImpl приховує від Domain джерело даних — Use Case не знає, чи прийшли дані з мережі чи кешу.

kotlin
// DTO — модель для мережі (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 — реалізація (Data)
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {

    override suspend fun getUser(id: Int): User {
        // Намагаємося дістати з кешу
        localDataSource.getUser(id)?.let { return it.toDomain() }
        // Якщо ні — завантажуємо з мережі
        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
fun UserDto.toDomain() = User(
    id = id,
    name = "$firstName $lastName",
    email = email
)

Стратегія кешування в Data Layer: RepositoryImpl спочатку перевіряє локальне сховище; за відсутності даних — завантажує з мережі та зберігає локально. Якщо мережа недоступна — повертає застарілі дані з позначкою isStale. Use Case в Domain не знає про стратегію — він отримує User через Repository.getUser(id). Зміна стратегії (наприклад, інвалідація кешу кожні 15 хвилин) не зачіпає Domain та Presentation.

Модульність в Android — Kotlin Multiplatform дозволяє винести Domain в окремий KMP-модуль без залежностей від Android SDK. Data — окремий модуль із залежністю від Domain. Presentation — Android-модуль із залежністю від Domain. Gradle залежності: domain (pure Kotlin), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Така модульність обов'язкова для великих проєктів — CI збирає Domain окремо, unit-тести Domain не потребують Android-емулятора.

Presentation Layer: ViewModels та Views

Presentation Layer — найзовнішніший шар Clean Architecture. Містить ViewModels (Android) / ObservableObject (iOS) та Views (Compose/SwiftUI). ViewModel викликає Use Case, отримує результат і перетворює в UI-стан (State). View підписується на State та відображає. Presentation залежить від Domain — імпортує Use Cases та Entities. Presentation не імпортує Data Layer — дані приходять через Use Case, який всередині використовує Repository.

ViewModel в Clean Architecture не містить бізнес-логіки — вона викликає Use Case. Якщо Use Case повертає User, ViewModel перетворює його в UserDisplayItem (name, emailFormatted, avatarUrl) — чисто презентаційну модель. Use Case не знає про DisplayItem — він повертає Entity. Це розділення дозволяє тестувати Use Case без UI та ViewModel без UseCase (через mock). В IT Sectr ми суворо дотримуємося: Use Case — бізнес-логіка, ViewModel — тільки презентація, View — тільки відображення.

kotlin
// Use Case (Domain) — чиста бізнес-логіка
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(id: Int): User {
        return repository.getUser(id)
    }
}

// ViewModel (Presentation) — тільки презентація
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 в Presentation шарі — теж частина зовнішнього кільця. Clean Architecture не приписує механізм навігації — це може бути NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) або Router (VIPER). Важливо: рішення про навігацію приймає Presentation, але навігація не повинна проникати в Use Case. Use Case повертає результат, ViewModel вирішує, на який екран перейти. В Clean Architecture навігація — деталь, яку можна замінити без зміни Domain.

Clean Architecture на iOS та Android: приклади коду

Clean Architecture на Android реалізується через модулі Gradle: domain (pure Kotlin), data (domain + Retrofit + Room), presentation (domain + Compose). Структура папок: 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) зв'язує шари: UserRepositoryImpl прив'язується до інтерфейсу UserRepository в domain-модулі.

Clean Architecture на iOS використовує SPM або Xcode групи без окремих модулів (через обмеження Xcode). Domain — папка з файлами, що не імпортують UIKit або SwiftUI. Data — папка з APIClient, CoreDataStack, RepositoryImpl. Presentation — папка з ViewModels та SwiftUI Views. DI через конструктор або збірку в App. Основний виклик — async/await через UseCase.execute() з перевіркою на MainActor для 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 в проєктах IT Sectr — наш стандарт для проєктів від 30 днів. Ми використовуємо трьохшарову архітектуру з Kotlin Multiplatform для Android/iOS з 2022 року. Domain — спільний KMP-модуль, Data — платформені модулі (Retrofit на Android, URLSession на iOS), Presentation — нативні UI. Це дає 60–80% спільного коду бізнес-логіки між iOS та Android, скорочуючи час розробки на 30–40% порівняно з двома окремими реалізаціями.

Часто задавані питання

Скільки шарів має бути в Clean Architecture?

Мінімально три: Domain, Data, Presentation. Для великих проєктів додають Framework (Android SDK/iOS UIKit-залежності) та Device (GPS, камера, датчики). Кількість шарів — не жорстке правило, а питання зручності. Головне — дотримуватися Dependency Rule: залежності спрямовані всередину, до Domain. Можна почати з трьох і додати шари по мірі зростання проєкту.

Clean Architecture збільшує кількість коду?

Так — на 30–50% порівняно з MVVM за рахунок виділення інтерфейсів репозиторіїв, Use Cases та мапперів. Для простого CRUD-застосунку це надлишково. Clean Architecture виправданий для проєктів зі складною бізнес-логікою, де тестованість та ізоляція шарів важливіші за швидкість розробки. Для MVP або прототипу використовуйте MVVM — Clean Architecture уповільнить запуск.

Чи можна комбінувати Clean Architecture з MVI?

Так, це поширена практика. Use Cases залишаються в Domain, а Presentation використовує MVI-цикл (Intent → Reducer → State). Data Layer — той самий, Domain — той самий. MVI в Presentation дає передбачуваний стан екрану, Clean Architecture — ізоляцію бізнес-логіки. Така комбінація використовується у великих проєктах з десятками розробників.

Чи потрібні Use Cases для кожного запиту до даних?

Use Case потрібен, коли операція включає бізнес-правило: валідацію, комбінування даних із двох джерел, розрахунок, логування, перевірку прав доступу. Простий запит getUser(id) без додаткової логіки може викликати Repository напряму з ViewModel. Однак для єдинообразності архітектури багато команд створюють Use Case для кожного публічного методу Repository — це додає 5–10% коду, але спрощує читання.

Як тестувати Clean Architecture?

Domain: Unit-тести Use Cases з mock Repository — чистий Kotlin/Swift без Android SDK. Data: інтеграційні тести RepositoryImpl з mock/fake DataSource. Presentation: тести ViewModel з mock UseCase. Завдяки Dependency Rule кожен шар тестується ізольовано. В IT Sectr покриття Domain сягає 95%, Data — 70–80%, Presentation — 60–70%.

Підсумки

  • Clean Architecture — три шари (Domain, Data, Presentation) з Dependency Rule всередину
  • Dependency Rule — Domain не знає про Data та Presentation, ізоляція через інтерфейси
  • Domain — Entities, Use Cases, Repository Interfaces — чистий Kotlin/Swift без фреймворків
  • Data — RepositoryImpl, DataSources (мережа, БД, кеш) — реалізація інтерфейсів Domain
  • Presentation — ViewModels, Views — тільки відображення, бізнес-логіка в Use Cases
  • Тестування — Domain покривається unit-тестами на 90–95%
  • KMP — Clean Architecture з Kotlin Multiplatform дає 60–80% спільного коду iOS + Android

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також