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. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође