Clean Architecture — Robert Martin (Uncle Bob) tarafından 2012 yılında önerilen, uygulamayı bağımsız katmanlara (Domain: Entities, Use Cases, Data: Repositories, DataSources ve Presentation: ViewModels, Views) ayıran çok katmanlı bir mimaridir. Temel ilke Bağımlılık Kuralıdır (Dependency Rule): bağımlılıklar içe doğru yönelir, dış katmanlar iç katmanlara bağlıdır, ancak tersi geçerli değildir. Clean Architecture, yüksek iş mantığı karmaşıklığına sahip projeler için mobil geliştirmede kullanılır. Daha fazla bilgi için The Clean Architecture kitabına bakın.
Önemli Noktalar
Clean Architecture — Robert Martin (Uncle Bob) tarafından 2012'de formüle edilen bir mimari desendir. Temel fikir, katı bir bağımlılık kuralıyla uygulamayı katmanlara bölmektir: katmanın içindeki kod, dışarıdaki kod hakkında hiçbir şey bilmez. Dış katmanlar (UI, çerçeveler, DB) uygulama detaylarıdır. İç katmanlar (iş mantığı, kurumsal kurallar) uygulamanın özüdür.
Mobil geliştirmede Clean Architecture Katmanları: 1) Domain — Varlıklar (iş nesneleri) ve Kullanım Senaryoları (kullanım senaryoları); 2) Data — RepositoryImpl (depo uygulamaları), DataSources (ağ, DB, önbellek); 3) Presentation — ViewModel'ler, Görünümler (Compose/SwiftUI). Domain, bağımlılığı olmayan en iç katmandır. Data, Domain'e bağlıdır (depo arayüzlerini uygular). Presentation, Domain'e bağlıdır (Kullanım Senaryolarını çağırır, sonuçlara abone olur).
| Katman | İçerir | Bağımlılıklar |
|---|---|---|
| Domain | Varlıklar, Kullanım Senaryoları, Depo Arayüzleri | Hiçbiri (saf Kotlin/Swift) |
| Data | RepositoryImpl, DataSources (API, DB, Önbellek) | Domain, Retrofit, Room, Ktor |
| Presentation | ViewModel'ler, Görünümler, Composables | Domain, Jetpack, SwiftUI |
Bağımlılık Kuralı — Clean Architecture'ın tek katı kuralıdır. Kaynak kodu yalnızca kendi içindeki bir katmana veya daha alttaki (merkeze daha yakın) bir katmana referans verebilir. Presentation, Domain'i içe aktarır. Domain, Data veya Presentation'ı içe AKTARMAZ. Bu, Bağımlılığı Tersine Çevirme İlkesi (Dependency Inversion Principle) ile elde edilir: Domain, Repository arayüzünü tanımlar, Data onu uygular. Presentation, belirli bir depoya değil, UseCase soyutlamasına bağlıdır.
Domain — uygulamanın en kararlı katmanıdır. Varlıklar, çerçevelerden bağımsız iş nesneleridir: User, Product, Order. Kullanım Senaryoları, tek bir invoke metodu (veya Kotlin'de operator fun invoke) olan ve bir senaryoyu uygulayan sınıflardır: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Depo Arayüzleri, Domain'de tanımlanan ve Data'da uygulanan veri erişim soyutlamalarıdır. Domain, Android SDK, iOS UIKit, Retrofit, Room içermez — yalnızca saf Kotlin veya Swift.
// Entity — iş nesnesi (Domain)
struct User: Equatable {
let id: Int
let name: String
let email: String
}
// Repository Interface — veri soyutlaması (Domain)
protocol UserRepository {
func getUser(id: Int) async throws -> User
func getUsers() async throws -> [User]
}
// Use Case — bir senaryo (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)
}
}
Kullanım Senaryosu — «tek metotlu bir sınıf» bir dogma değil, pratik bir öneridir. Bir Kullanım Senaryosu daha karmaşık hale geldiğinde (doğrulama + günlükleme + depo çağrısı), metotları anlamlarına göre gruplandırılır: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. Önemli olan, bir Kullanım Senaryosunun verilerin nereden geldiğini (ağ, DB, önbellek) veya kimin görüntülediğini (Compose, SwiftUI) bilmemesidir. IT Sectr'de, bir iş kuralı, doğrulama veya iki kaynaktan veri birleştirmesi olan her işlem için bir Kullanım Senaryosu ayırırız.
Domain Saflığı, katman sınırlarında DTO eşlemesiyle elde edilir. Data katmanı JSON modelleri (DTO) alır, bunları Domain Entity'lerine eşler. Presentation, Domain Entity'lerini alır, ViewModel'lere (DisplayItem) eşler. Bir Domain Entity'si asla Retrofit, Room veya Codable ek açıklamaları içermez — bu, DB'yi Room'dan Realm'a değiştirirken veya Retrofit'i Ktor ile değiştirirken katmanın değiştirilmesi gerekmeyeceğini garanti eder.
Data Katmanı — Domain'de tanımlanan arayüzlerin uygulanmasıdır. RepositoryImpl (UserRepository'yi uygulayan sınıflar) ve DataSources'u (RemoteDataSource — API, LocalDataSource — DB, CacheDataSource — SharedPreferences/NSUserDefaults) içerir. Data katmanı, Domain'e (depo arayüzlerini ve Entity'leri içe aktarır) ve çerçevelere (Retrofit, Room, Ktor, CoreData) bağlıdır. RepositoryImpl, veri kaynağını Domain'den gizler — Kullanım Senaryosu, verilerin ağdan mı yoksa önbellekten mi geldiğini bilmez.
// DTO — ağ için model (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 — uygulama (Data)
class UserRepositoryImpl(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun getUser(id: Int): User {
// Önbellekten almayı dene
localDataSource.getUser(id)?.let { return it.toDomain() }
// Yoksa — ağdan yükle
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 dönüşümü
fun UserDto.toDomain() = User(
id = id,
name = "$firstName $lastName",
email = email
)
Data Katmanında Önbellekleme Stratejisi: RepositoryImpl önce yerel depolamayı kontrol eder; veri bulunamazsa ağdan yükler ve yerel olarak kaydeder. Ağ kullanılamıyorsa, isStale bayrağıyla eski verileri döndürür. Domain'deki Kullanım Senaryosu stratejiyi bilmez — User'ı Repository.getUser(id) aracılığıyla alır. Stratejiyi değiştirmek (örneğin, her 15 dakikada bir önbellek geçersizleştirme) Domain ve Presentation'ı etkilemez.
Android'de Modülerlik — Kotlin Multiplatform, Domain'in Android SDK bağımlılıkları olmadan ayrı bir KMP modülüne çıkarılmasına olanak tanır. Data, Domain'e bağımlı ayrı bir modüldür. Presentation, Domain'e bağımlı bir Android modülüdür. Gradle bağımlılıkları: domain (saf Kotlin), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Bu tür bir modülerlik büyük projeler için gereklidir — CI, Domain'i ayrı ayrı oluşturur, Domain birim testleri Android öykünücüsü gerektirmez.
Presentation Katmanı — Clean Architecture'ın en dış katmanıdır. ViewModel'leri (Android) / ObservableObject'leri (iOS) ve Görünümleri (Compose/SwiftUI) içerir. ViewModel, bir Kullanım Senaryosunu çağırır, sonucu alır ve UI durumuna dönüştürür. Görünüm, Duruma abone olur ve işler. Presentation, Domain'e bağlıdır — Kullanım Senaryolarını ve Entity'leri içe aktarır. Presentation, Data Katmanını içe aktarmaz — veriler, dahili olarak bir Depo kullanan Kullanım Senaryosu aracılığıyla gelir.
Clean Architecture'da ViewModel iş mantığı içermez — Kullanım Senaryosunu çağırır. Bir Kullanım Senaryosu User döndürürse, ViewModel onu UserDisplayItem'a (name, emailFormatted, avatarUrl) dönüştürür — tamamen sunumsal bir model. Kullanım Senaryosu DisplayItem'ı bilmez — bir Entity döndürür. Bu ayrım, Kullanım Senaryosunun UI olmadan ve ViewModel'in UseCase olmadan (mock'lar aracılığıyla) test edilmesini sağlar. IT Sectr'de kesinlikle şunu takip ederiz: Kullanım Senaryosu — iş mantığı, ViewModel — yalnızca sunum, Görünüm — yalnızca görüntüleme.
// Use Case (Domain) — saf iş mantığı
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: Int): User {
return repository.getUser(id)
}
}
// ViewModel (Presentation) — yalnızca sunum
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
}
Navigasyon, Presentation katmanında da dış halkanın bir parçasıdır. Clean Architecture bir navigasyon mekanizması önermez — NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) veya Router (VIPER) olabilir. Önemli olan, navigasyon kararlarının Presentation tarafından alınması, ancak navigasyonun Kullanım Senaryosuna nüfuz etmemesidir. Kullanım Senaryosu bir sonuç döndürür, ViewModel hangi ekrana gidileceğine karar verir. Clean Architecture'da navigasyon, Domain değiştirilmeden değiştirilebilen bir detaydır.
Android'de Clean Architecture, Gradle modülleri aracılığıyla uygulanır: domain (saf Kotlin), data (domain + Retrofit + Room), presentation (domain + Compose). Klasör yapısı: 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) katmanları bağlar: UserRepositoryImpl, domain modülündeki UserRepository arayüzüne bağlanır.
iOS'te Clean Architecture, Xcode sınırlamaları nedeniyle ayrı modüller olmadan SPM veya Xcode gruplarını kullanır. Domain — UIKit veya SwiftUI içe aktarmayan dosyaların bulunduğu bir klasör. Data — APIClient, CoreDataStack, RepositoryImpl içeren bir klasör. Presentation — ViewModel'ler ve SwiftUI Görünümleri içeren bir klasör. DI, oluşturucu veya Uygulama içinde birleştirme yoluyla yapılır. Ana çağrı, UI güncellemeleri için MainActor kontrolü ile UseCase.execute() üzerinden async/await'tir.
// 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")
}
}
}
}
IT Sectr Projelerinde Clean Architecture — 30 gün ve üzeri projeler için standardımızdır. 2022'den beri Android/iOS için Kotlin Multiplatform ile üç katmanlı mimari kullanıyoruz. Domain — paylaşılan bir KMP modülü, Data — platform modülleri (Android'de Retrofit, iOS'te URLSession), Presentation — yerel UI. Bu, iOS ve Android arasında %60–80 oranında paylaşılan iş mantığı kodu sağlar ve iki ayrı uygulamaya kıyasla geliştirme süresini %30–40 oranında azaltır.
Sıkça Sorulan Sorular
En az üç: Domain, Data, Presentation. Büyük projeler için Framework (Android SDK/iOS UIKit bağımlılıkları) ve Device (GPS, kamera, sensörler) eklenir. Katman sayısı katı bir kural değil, kolaylık meselesidir. Önemli olan Bağımlılık Kuralına uymaktır: bağımlılıklar içe, Domain'e doğru yönelir. Üç katmanla başlayabilir ve proje büyüdükçe katman ekleyebilirsiniz.
Evet — depo arayüzleri, Kullanım Senaryoları ve eşleyicilerin çıkarılması nedeniyle MVVM'ye kıyasla %30–50 oranında artırır. Basit bir CRUD uygulaması için bu aşırıdır. Clean Architecture, test edilebilirlik ve katman izolasyonunun geliştirme hızından daha önemli olduğu karmaşık iş mantığına sahip projeler için uygundur. MVP veya prototipler için MVVM kullanın — Clean Architecture başlangıcı yavaşlatacaktır.
Evet, bu yaygın bir uygulamadır. Kullanım Senaryoları Domain'de kalırken, Presentation MVI döngüsünü (Intent → Reducer → State) kullanır. Data Katmanı aynı kalır, Domain aynı kalır. Presentation'da MVI öngörülebilir ekran durumu sağlar, Clean Architecture iş mantığı izolasyonu sağlar. Bu kombinasyon, onlarca geliştiricinin olduğu büyük projelerde kullanılır.
Bir Kullanım Senaryosu, işlem bir iş kuralı içerdiğinde gereklidir: doğrulama, iki kaynaktan veri birleştirme, hesaplama, günlükleme, erişim izni kontrolü. Ek mantık gerektirmeyen basit bir getUser(id) isteği, Depoyu doğrudan ViewModel'den çağırabilir. Ancak, mimari tutarlılık için birçok ekip, Deponun her genel metodu için bir Kullanım Senaryosu oluşturur — bu, %5–10 kod ekler ancak okumayı basitleştirir.
Domain: Mock Depo ile Kullanım Senaryolarının birim testleri — Android SDK olmadan saf Kotlin/Swift. Data: Mock/sahte DataSource ile RepositoryImpl'in entegrasyon testleri. Presentation: Mock UseCase ile ViewModel testleri. Bağımlılık Kuralı sayesinde her katman ayrı ayrı test edilir. IT Sectr'de Domain kapsamı %95'e, Data — %70–80'e, Presentation — %60–70'e ulaşır.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun