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 чрез конструктор или assembly в 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 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също