Clean Architecture — πολυεπίπεδη αρχιτεκτονική που προτάθηκε από τον Robert Martin (Uncle Bob) το 2012, χωρίζοντας την εφαρμογή σε ανεξάρτητα επίπεδα: Domain (Entities, Use Cases), Data (Repositories, DataSources) και Presentation (ViewModels, Views). Η βασική αρχή — Dependency Rule: οι εξαρτήσεις κατευθύνονται προς τα μέσα, τα εξωτερικά επίπεδα εξαρτώνται από τα εσωτερικά, αλλά όχι το αντίθετο. Η Clean Architecture εφαρμόζεται στην ανάπτυξη κινητών για έργα με υψηλή πολυπλοκότητα επιχειρηματικής λογικής. Περισσότερα — στο βιβλίο The Clean Architecture.
Κύρια σημεία
Clean Architecture — αρχιτεκτονικό πρότυπο που διατυπώθηκε από τον Robert Martin (Uncle Bob) το 2012. Η βασική ιδέα — διαίρεση της εφαρμογής σε επίπεδα με αυστηρό κανόνα εξαρτήσεων: ο κώδικας μέσα σε ένα επίπεδο δεν γνωρίζει για τον κώδικα έξω. Τα εξωτερικά επίπεδα (UI, frameworks, βάσεις δεδομένων) — λεπτομέρειες υλοποίησης. Τα εσωτερικά επίπεδα (επιχειρηματική λογική, κανόνες επιχείρησης) — η ουσία της εφαρμογής.
Επίπεδα Clean Architecture στην ανάπτυξη κινητών: 1) Domain — Entities (επιχειρηματικά αντικείμενα) και Use Cases (περιπτώσεις χρήσης); 2) Data — RepositoryImpl (υλοποίηση αποθετηρίων), DataSources (δίκτυο, βάση δεδομένων, cache); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain — το πιο εσωτερικό επίπεδο, χωρίς εξαρτήσεις. Το Data εξαρτάται από το Domain (υλοποιεί τις διεπαφές αποθετηρίων). Το Presentation εξαρτάται από το Domain (καλεί Use Cases, εγγράφεται στο αποτέλεσμα).
| Επίπεδο | Περιέχει | Εξαρτήσεις |
|---|---|---|
| Domain | Entities, Use Cases, Repository Interfaces | Καμία (καθαρό Kotlin/Swift) |
| Data | RepositoryImpl, DataSources (API, DB, Cache) | Domain, Retrofit, Room, Ktor |
| Presentation | ViewModels, Views, Composables | Domain, Jetpack, SwiftUI |
Dependency Rule — ο μοναδικός αυστηρός κανόνας της Clean Architecture. Ο πηγαίος κώδικας μπορεί να αναφέρεται μόνο στο επίπεδο εντός του ή στο επίπεδο κάτω (πιο κοντά στο κέντρο). Το Presentation εισάγει το Domain. Το Domain ΔΕΝ εισάγει το Data ή το Presentation. Αυτό επιτυγχάνεται μέσω αντιστροφής εξαρτήσεων (Dependency Inversion Principle): το Domain ορίζει τη διεπαφή Repository, το Data την υλοποιεί. Το Presentation εξαρτάται από την αφαίρεση UseCase, όχι από το συγκεκριμένο αποθετήριο.
Domain — το πιο σταθερό επίπεδο της εφαρμογής. Entities — επιχειρηματικά αντικείμενα ανεξάρτητα από frameworks: 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.
// 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 δεν πρέπει να γνωρίζει από πού προέρχονται τα δεδομένα (δίκτυο, βάση δεδομένων, cache) και ποιος τα εμφανίζει (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 — υλοποίηση των διεπαφών που ορίζονται στο Domain. Περιέχει RepositoryImpl (κλάσεις που υλοποιούν το UserRepository) και DataSources (RemoteDataSource — API, LocalDataSource — βάση δεδομένων, CacheDataSource — SharedPreferences/NSUserDefaults). Το επίπεδο Data εξαρτάται από το Domain (εισάγει τις διεπαφές αποθετηρίων και Entities) και από frameworks (Retrofit, Room, Ktor, CoreData). Το RepositoryImpl κρύβει από το Domain την πηγή δεδομένων — το Use Case δεν γνωρίζει αν τα δεδομένα προέρχονται από το δίκτυο ή το cache.
// 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). Η αλλαγή στρατηγικής (π.χ. ακύρωση cache κάθε 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 tests του Domain δεν απαιτούν εξομοιωτή Android.
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 — μόνο εμφάνιση.
// 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 στο 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.
// 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% σε σύγκριση με δύο ξεχωριστές υλοποιήσεις.
Συχνές Ερωτήσεις
Ελάχιστα τρία: Domain, Data, Presentation. Για μεγάλα έργα προστίθενται Framework (εξαρτήσεις Android SDK/iOS UIKit) και Device (GPS, κάμερα, αισθητήρες). Ο αριθμός επιπέδων — δεν είναι αυστηρός κανόνας, αλλά θέμα ευκολίας. Το σημαντικό είναι να τηρείται ο Dependency Rule: οι εξαρτήσεις κατευθύνονται προς τα μέσα, προς το Domain. Μπορείτε να ξεκινήσετε με τρία και να προσθέτετε επίπεδα καθώς το έργο μεγαλώνει.
Ναι — κατά 30–50% σε σύγκριση με MVVM λόγω διαχωρισμού διεπαφών αποθετηρίων, Use Cases και mappers. Για μια απλή εφαρμογή CRUD είναι υπερβολικό. Η Clean Architecture δικαιολογείται για έργα με πολύπλοκη επιχειρηματική λογική, όπου η δυνατότητα δοκιμής και η απομόνωση επιπέδων είναι σημαντικότερες από την ταχύτητα ανάπτυξης. Για MVP ή πρωτότυπο χρησιμοποιήστε MVVM — η Clean Architecture θα επιβραδύνει την εκκίνηση.
Ναι, είναι κοινή πρακτική. Τα Use Cases παραμένουν στο Domain και το Presentation χρησιμοποιεί τον κύκλο MVI (Intent → Reducer → State). Data Layer — το ίδιο, Domain — το ίδιο. Το MVI στο Presentation δίνει προβλέψιμη κατάσταση οθόνης, η Clean Architecture — απομόνωση επιχειρηματικής λογικής. Αυτός ο συνδυασμός χρησιμοποιείται σε μεγάλα έργα με δεκάδες προγραμματιστές.
Το Use Case χρειάζεται όταν η λειτουργία περιλαμβάνει επιχειρηματικό κανόνα: επικύρωση, συνδυασμό δεδομένων από δύο πηγές, υπολογισμό, καταγραφή, έλεγχο δικαιωμάτων πρόσβασης. Ένα απλό αίτημα getUser(id) χωρίς πρόσθετη λογική μπορεί να καλέσει το Repository απευθείας από το ViewModel. Ωστόσο, για ομοιομορφία αρχιτεκτονικής, πολλές ομάδες δημιουργούν Use Case για κάθε δημόσια μέθοδο Repository — αυτό προσθέτει 5–10% κώδικα αλλά απλοποιεί την ανάγνωση.
Domain: unit tests 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%.
Περίληψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης