Clean Architecture — arhitectură multistrat propusă de Robert Martin (Uncle Bob) în 2012, care împarte aplicația în straturi independente: Domain (Entities, Use Cases), Data (Repositories, DataSources) și Presentation (ViewModels, Views). Principiul principal — Dependency Rule: dependențele sunt direcționate spre interior, straturile externe depind de cele interne, dar nu invers. Clean Architecture se aplică în dezvoltarea mobilă pentru proiecte cu complexitate ridicată a logicii de afaceri. Mai multe — în cartea The Clean Architecture.
Principalele
Clean Architecture — model arhitectural formulat de Robert Martin (Uncle Bob) în 2012. Ideea principală — împărțirea aplicației în straturi cu regulă strictă de dependență: codul din interiorul stratului nu știe despre codul din exterior. Straturile externe (UI, frameworkuri, baze de date) — detalii de implementare. Straturile interne (logică de afaceri, reguli de întreprindere) — esența aplicației.
Straturile Clean Architecture în dezvoltarea mobilă: 1) Domain — Entities (obiecte de afaceri) și Use Cases (cazuri de utilizare); 2) Data — RepositoryImpl (implementarea repozitoriilor), DataSources (rețea, bază de date, cache); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain — cel mai interior strat, fără dependențe. Data depinde de Domain (implementează interfețele repozitoriilor). Presentation depinde de Domain (apelează Use Cases, se abonează la rezultat).
| Strat | Conține | Dependențe |
|---|---|---|
| Domain | Entities, Use Cases, Repository Interfaces | Niciunele (Kotlin/Swift pur) |
| Data | RepositoryImpl, DataSources (API, DB, Cache) | Domain, Retrofit, Room, Ktor |
| Presentation | ViewModels, Views, Composables | Domain, Jetpack, SwiftUI |
Dependency Rule — singura regulă strictă a Clean Architecture. Codul sursă poate face referire doar la stratul din interiorul său sau la stratul inferior (mai aproape de centru). Presentation importă Domain. Domain NU importă Data sau Presentation. Acest lucru se realizează prin inversarea dependențelor (Dependency Inversion Principle): Domain definește interfața Repository, Data o implementează. Presentation depinde de abstractizarea UseCase, nu de repozitoriul concret.
Domain — cel mai stabil strat al aplicației. Entities — obiecte de afaceri independente de frameworkuri: User, Product, Order. Use Cases — clase cu o singură metodă invoke (sau operator fun invoke în Kotlin), care implementează un singur scenariu: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Repository Interfaces — abstractizări ale accesului la date, definite în Domain, implementate în Data. Domain nu conține Android SDK, iOS UIKit, Retrofit, Room — doar Kotlin sau Swift pur.
// Entity — obiect de afaceri (Domain)
struct User: Equatable {
let id: Int
let name: String
let email: String
}
// Repository Interface — abstractizare date (Domain)
protocol UserRepository {
func getUser(id: Int) async throws -> User
func getUsers() async throws -> [User]
}
// Use Case — un scenariu (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 — «clasă cu o singură metodă» — nu un dogm, ci o recomandare practică. Când Use Case devine mai complex (validare + logare + apelare repozitoriu), metodele sale se grupează după sens: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. Important — Use Case nu trebuie să știe de unde vin datele (rețea, bază de date, cache) și cine le afișează (Compose, SwiftUI). În IT Sectr alocăm un Use Case pentru fiecare operație care are o regulă de afaceri, validare sau combinare de date din două surse.
Puritatea Domain se realizează prin maparea DTO la granița straturilor. Stratul Data primește modele JSON (DTO), le mapează în Entity Domain. Presentation primește Entity Domain, îl mapează în ViewModel (DisplayItem). Entity Domain nu conține niciodată adnotări Retrofit, Room, Codable — aceasta garantează că stratul nu va trebui modificat la schimbarea bazei de date de la Room la Realm sau la înlocuirea Retrofit cu Ktor.
Data Layer — implementarea interfețelor definite în Domain. Conține RepositoryImpl (clase care implementează UserRepository) și DataSources (RemoteDataSource — API, LocalDataSource — bază de date, CacheDataSource — SharedPreferences/NSUserDefaults). Stratul Data depinde de Domain (importă interfețele repozitoriilor și Entities) și de frameworkuri (Retrofit, Room, Ktor, CoreData). RepositoryImpl ascunde de Domain sursa datelor — Use Case nu știe dacă datele vin din rețea sau cache.
// DTO — model pentru rețea (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 — implementare (Data)
class UserRepositoryImpl(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun getUser(id: Int): User {
// Încercăm să obținem din cache
localDataSource.getUser(id)?.let { return it.toDomain() }
// Dacă nu — încărcăm din rețea
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 — transformare DTO ↔ Domain
fun UserDto.toDomain() = User(
id = id,
name = "$firstName $lastName",
email = email
)
Strategia de cache în Data Layer: RepositoryImpl verifică mai întâi stocarea locală, în lipsa datelor — încarcă din rețea și salvează local. Dacă rețeaua este indisponibilă — returnează date învechite cu marcajul isStale. Use Case în Domain nu știe despre strategie — primește User prin Repository.getUser(id). Schimbarea strategiei (de exemplu, invalidarea cache-ului la fiecare 15 minute) nu afectează Domain și Presentation.
Modularitatea în Android — Kotlin Multiplatform permite mutarea Domain într-un modul KMP separat fără dependențe de Android SDK. Data — modul separat cu dependență de Domain. Presentation — modul Android cu dependență de Domain. Dependențe Gradle: domain (pure Kotlin), data (domain + Retrofit + Room), app (domain + presentation + Hilt). O astfel de modularitate este obligatorie pentru proiecte mari — CI construiește Domain separat, testele unitare Domain nu necesită emulator Android.
Presentation Layer — cel mai exterior strat al Clean Architecture. Conține ViewModels (Android) / ObservableObject (iOS) și Views (Compose/SwiftUI). ViewModel apelează Use Case, primește rezultatul și îl transformă în stare UI (State). View se abonează la State și afișează. Presentation depinde de Domain — importă Use Cases și Entities. Presentation nu importă Data Layer — datele vin prin Use Case, care intern folosește Repository.
ViewModel în Clean Architecture nu conține logică de afaceri — apelează Use Case. Dacă Use Case returnează User, ViewModel îl transformă în UserDisplayItem (name, emailFormatted, avatarUrl) — un model pur de prezentare. Use Case nu știe de DisplayItem — returnează Entity. Această separare permite testarea Use Case fără UI și a ViewModel fără UseCase (prin mock). În IT Sectr respectăm strict: Use Case — logică de afaceri, ViewModel — doar prezentare, View — doar afișare.
// Use Case (Domain) — logică de afaceri pură
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: Int): User {
return repository.getUser(id)
}
}
// ViewModel (Presentation) — doar prezentare
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 în stratul Presentation — parte a inelului exterior. Clean Architecture nu prescrie un mecanism de navigare — poate fi NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) sau Router (VIPER). Important: decizia de navigare este luată de Presentation, dar navigarea nu trebuie să pătrundă în Use Case. Use Case returnează rezultatul, ViewModel decide pe ce ecran să navigheze. În Clean Architecture navigarea este un detaliu care poate fi înlocuit fără modificarea Domain.
Clean Architecture pe Android se implementează prin module Gradle: domain (pure Kotlin), data (domain + Retrofit + Room), presentation (domain + Compose). Structura folderelor: 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) conectează straturile: UserRepositoryImpl este legat de interfața UserRepository în modulul domain.
Clean Architecture pe iOS folosește SPM sau grupuri Xcode fără module separate (din cauza limitărilor Xcode). Domain — folder cu fișiere care nu importă UIKit sau SwiftUI. Data — folder cu APIClient, CoreDataStack, RepositoryImpl. Presentation — folder cu ViewModels și SwiftUI Views. DI prin constructor sau assembly în App. Apelul principal — async/await prin UseCase.execute() cu verificare MainActor pentru actualizări UI.
// 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 în proiectele IT Sectr — standardul nostru pentru proiecte de la 30 de zile. Folosim arhitectura pe trei straturi cu Kotlin Multiplatform pentru Android/iOS din 2022. Domain — modul KMP comun, Data — module de platformă (Retrofit pe Android, URLSession pe iOS), Presentation — UI nativ. Aceasta oferă 60–80% cod comun al logicii de afaceri între iOS și Android, reducând timpul de dezvoltare cu 30–40% față de două implementări separate.
Întrebări frecvente
Minim trei: Domain, Data, Presentation. Pentru proiecte mari se adaugă Framework (dependențe Android SDK/iOS UIKit) și Device (GPS, cameră, senzori). Numărul de straturi — nu o regulă fixă, ci o chestiune de confort. Important este să se respecte Dependency Rule: dependențele sunt direcționate spre interior, către Domain. Se poate începe cu trei și adăuga straturi pe măsură ce proiectul crește.
Da — cu 30–50% față de MVVM datorită separării interfețelor de repozitorii, Use Cases și maperelor. Pentru o aplicație CRUD simplă este excesivă. Clean Architecture se justifică pentru proiecte cu logică de afaceri complexă, unde testabilitatea și izolarea straturilor sunt mai importante decât viteza de dezvoltare. Pentru MVP sau prototip folosiți MVVM — Clean Architecture va încetini lansarea.
Da, este o practică comună. Use Cases rămân în Domain, iar Presentation folosește ciclul MVI (Intent → Reducer → State). Data Layer — același, Domain — același. MVI în Presentation oferă o stare de ecran previzibilă, Clean Architecture — izolarea logicii de afaceri. Această combinație se folosește în proiecte mari cu zeci de dezvoltatori.
Use Case este necesar când operația include o regulă de afaceri: validare, combinare de date din două surse, calcul, logare, verificare a drepturilor de acces. O simplă cerere getUser(id) fără logică suplimentară poate apela Repository direct din ViewModel. Totuși, pentru uniformitatea arhitecturii, multe echipe creează un Use Case pentru fiecare metodă publică a Repository — aceasta adaugă 5–10% cod, dar simplifică citirea.
Domain: teste unitare ale Use Cases cu mock Repository — Kotlin/Swift pur fără Android SDK. Data: teste de integrare ale RepositoryImpl cu mock/fake DataSource. Presentation: teste ViewModel cu mock UseCase. Datorită Dependency Rule, fiecare strat se testează izolat. În IT Sectr acoperirea Domain atinge 95%, Data — 70–80%, Presentation — 60–70%.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și