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, DB, Cache)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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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