Tüm Yazılar
KategoriArchitecture
Okuma Süresi
20 dk
Yayın Tarihi
...
Kelime Sayısı
2.269kelime

Kahveni hazırla - bu içerikli bir makale!

SOLID prensipleri ve Clean Architecture ile ölçeklenebilir iOS uygulamaları geliştirme rehberi.

iOS'ta Clean Architecture

"Bu kodu nereye koyayım?" sorusu, her iOS geliştiricinin kariyerinde en sık sorduğu sorulardan biri. ViewController 3000 satır mı oldu? ViewModel'de network çağrısı mı yapılıyor? Test yazmak imkansız mı? Bu sorunların hepsinin tek bir cevabı var: Clean Architecture.

Robert C. Martin'in (Uncle Bob) ortaya koyduğu bu mimari yaklaşım, yazılım geliştirmede test edilebilirlik, bağımsızlık ve sürdürülebilirlik sağlıyor. Bu rehberde Clean Architecture'ı iOS perspektifinden, gerçek production örnekleriyle ve adım adım uygulayacağız.

💡 Hızlı Not: Bu yazı Uncle Bob'un "Clean Architecture" kitabı, Apple'ın resmi mimari önerileri ve Uber, Spotify gibi şirketlerin production deneyimlerinden derlendi.

İçindekiler

  1. Clean Architecture Nedir?
  2. Dependency Rule: Tek ve Tartışılmaz Kural
  3. Katman 1: Entities (Domain Layer)
  4. Katman 2: Use Cases (Application Layer)
  5. Katman 3: Interface Adapters (Presentation)
  6. Katman 4: Framework & Drivers (Infrastructure)
  7. Dependency Injection: Katmanları Birleştirmek
  8. SOLID Prensipleri Pratikte
  9. MVC vs MVVM vs VIPER vs Clean Architecture
  10. Testing Stratejisi
  11. Production Checklist

Clean Architecture Nedir? {#clean-architecture-nedir}

Clean Architecture, uygulamanızı konsantrik katmanlara ayırır. Her katman, sadece daha içteki katmanlara bağımlı olabilir - dıştaki katmanlara bağımlı olamaz. Bu basit kural, tüm mimarinin temelidir.

swift
1┌─────────────────────────────────────────────┐
2│ Framework & Drivers(UI, DB) │
3│ ┌─────────────────────────────────────┐ │
4│ │ Interface Adapters(ViewModels) │ │
5│ │ ┌─────────────────────────────┐ │ │
6│ │ │ Use Cases(Business) │ │ │
7│ │ │ ┌─────────────────────┐ │ │ │
8│ │ │ │ Entities(Core) │ │ │ │
9│ │ │ └─────────────────────┘ │ │ │
10│ │ └─────────────────────────┘ │ │
11│ └─────────────────────────────────────┘ │
12└─────────────────────────────────────────────┘
13 
14 Bağımlılık yönü: Her zaman DIŞTAN İÇE →

Neden bu kadar önemli? Çünkü bu yapı sayesinde:

  • UI framework değişse bile: iş mantığın etkilenmez (UIKit → SwiftUI geçişi)
  • Database değişse bile: use case'lerin etkilenmez (Core Data → SwiftData)
  • Her katmanı bağımsız test: edebilirsin (mock'larla)

Dış Kaynaklar:


Dependency Rule: Tek ve Tartışılmaz Kural {#dependency-rule}

Clean Architecture'ın tek kuralı: Bağımlılıklar her zaman dıştan içe doğru akar. İç katmanlar, dış katmanların varlığından haberdar bile olmamalı.

Bu kuralı ihlal ettiğin an, tüm mimari çöker. Peki pratikte bu nasıl uygulanır? Protocol'ler ile:

swift
1// ❌ YANLIŞ: Domain katmanı, Infrastructure'a bağımlı
2// Domain/UseCases/FetchUsersUseCase.swift
3import CoreData // ← Domain katmanında framework import = KURAL İHLALİ!
4 
5class FetchUsersUseCase {
6 let context: NSManagedObjectContext // ← CoreData bağımlılığı
7 
8 func execute() -> [User] {
9 let request = NSFetchRequest<UserEntity>(entityName: "User")
10 return try! context.fetch(request).map { $0.toDomain() }
11 }
12}
13 
14// ✅ DOĞRU: Domain katmanı sadece protokol tanımlar
15// Domain/Protocols/UserRepositoryProtocol.swift
16protocol UserRepositoryProtocol {
17 func fetchAll() async throws -> [User]
18 func save(_ user: User) async throws
19 func delete(id: UUID) async throws
20}
21 
22// Domain/UseCases/FetchUsersUseCase.swift
23// import YOK - pure Swift!
24final class FetchUsersUseCase {
25 private let repository: UserRepositoryProtocol // ← Sadece protokol
26 
27 init(repository: UserRepositoryProtocol) {
28 self.repository = repository
29 }
30 
31 func execute(sortBy: SortOption = .name) async throws -> [User] {
32 let users = try await repository.fetchAll()
33 return sort(users, by: sortBy)
34 }
35}

Katman 1: Entities (Domain Layer) {#entities}

En içteki katman - uygulamanın kalbi. İş mantığı kurallarını içerir. Hiçbir framework'e bağımlı değildir. Saf Swift.

swift
1// Domain/Entities/User.swift - PURE SWIFT, import YOK!
2struct User: Identifiable, Equatable, Sendable {
3 let id: UUID
4 var name: String
5 var email: String
6 var avatar: URL?
7 let createdAt: Date
8 var role: Role
9 
10 enum Role: String, Sendable {
11 case admin, editor, viewer
12 }
13 
14 // İş mantığı Entity'de yaşar
15 func validate() -> ValidationResult {
16 var errors: [String] = []
17 
18 if name.trimmingCharacters(in: .whitespaces).isEmpty {
19 errors.append("İsim boş olamaz")
20 }
21 if !email.contains("@") || !email.contains(".") {
22 errors.append("Geçersiz email formatı")
23 }
24 if name.count > 100 {
25 errors.append("İsim 100 karakterden uzun olamaz")
26 }
27 
28 return errors.isEmpty ? .valid : .invalid(errors)
29 }
30 
31 enum ValidationResult {
32 case valid
33 case invalid([String])
34 }
35}
36 
37// Domain/Entities/Product.swift
38struct Product: Identifiable, Equatable, Sendable {
39 let id: UUID
40 var name: String
41 var description: String
42 var price: Decimal
43 var stock: Int
44 var category: Category
45 let createdAt: Date
46 
47 enum Category: String, CaseIterable, Sendable {
48 case electronics, clothing, food, books
49 }
50 
51 // İş kuralı: stok kontrolü
52 var isAvailable: Bool { stock > 0 }
53 
54 // İş kuralı: indirim hesaplama
55 func discountedPrice(percentage: Decimal) -> Decimal {
56 guard percentage > 0 && percentage <= 100 else { return price }
57 return price * (1 - percentage / 100)
58 }
59}
🎯 Best Practice: Entity'ler immutable olmalı (mümkünse let kullan). Mutation gereken yerlerde yeni instance oluştur. Bu, thread safety ve debugging kolaylığı sağlar.

Katman 2: Use Cases (Application Layer) {#use-cases}

Use Case'ler, uygulamanın ne yapacağını tanımlar. Her Use Case tek bir operasyonu temsil eder (Single Responsibility).

swift
1// Domain/UseCases/FetchProductsUseCase.swift
2protocol FetchProductsUseCaseProtocol: Sendable {
3 func execute(filter: ProductFilter?) async throws -> [Product]
4}
5 
6struct ProductFilter: Sendable {
7 var category: Product.Category?
8 var minPrice: Decimal?
9 var maxPrice: Decimal?
10 var inStockOnly: Bool = true
11}
12 
13final class FetchProductsUseCase: FetchProductsUseCaseProtocol {
14 private let repository: ProductRepositoryProtocol
15 
16 init(repository: ProductRepositoryProtocol) {
17 self.repository = repository
18 }
19 
20 func execute(filter: ProductFilter? = nil) async throws -> [Product] {
21 var products = try await repository.fetchAll()
22 
23 if let filter {
24 if let category = filter.category {
25 products = products.filter { $0.category == category }
26 }
27 if let minPrice = filter.minPrice {
28 products = products.filter { $0.price >= minPrice }
29 }
30 if let maxPrice = filter.maxPrice {
31 products = products.filter { $0.price <= maxPrice }
32 }
33 if filter.inStockOnly {
34 products = products.filter { $0.isAvailable }
35 }
36 }
37 
38 return products.sorted { $0.createdAt > $1.createdAt }
39 }
40}
41 
42// Her Use Case TEK bir iş yapar - 50 satırı geçmesin!
43final class AddToCartUseCase {
44 private let cartRepository: CartRepositoryProtocol
45 private let productRepository: ProductRepositoryProtocol
46 
47 init(cart: CartRepositoryProtocol, product: ProductRepositoryProtocol) {
48 self.cartRepository = cart
49 self.productRepository = product
50 }
51 
52 func execute(productId: UUID, quantity: Int) async throws {
53 let product = try await productRepository.fetch(id: productId)
54 
55 guard product.isAvailable else {
56 throw DomainError.outOfStock
57 }
58 guard product.stock >= quantity else {
59 throw DomainError.insufficientStock(available: product.stock)
60 }
61 
62 try await cartRepository.addItem(productId: productId, quantity: quantity)
63 }
64}

Katman 3: Interface Adapters (Presentation) {#interface-adapters}

ViewModel'ler ve Presenter'lar bu katmanda. Use Case'leri çağırır, sonuçları UI'a uygun formata dönüştürür.

swift
1// Presentation/ViewModels/ProductListViewModel.swift
2@MainActor
3final class ProductListViewModel: ObservableObject {
4 @Published private(set) var products: [ProductDisplayItem] = []
5 @Published private(set) var isLoading = false
6 @Published private(set) var error: String?
7 @Published var selectedCategory: Product.Category?
8 
9 private let fetchProductsUseCase: FetchProductsUseCaseProtocol
10 
11 // Dependency Injection via init
12 init(fetchProductsUseCase: FetchProductsUseCaseProtocol) {
13 self.fetchProductsUseCase = fetchProductsUseCase
14 }
15 
16 func loadProducts() async {
17 isLoading = true
18 error = nil
19 
20 do {
21 let filter = ProductFilter(category: selectedCategory)
22 let domainProducts = try await fetchProductsUseCase.execute(filter: filter)
23 
24 // Domain → Presentation mapping
25 products = domainProducts.map { product in
26 ProductDisplayItem(
27 id: product.id,
28 name: product.name,
29 formattedPrice: formatPrice(product.price),
30 availability: product.isAvailable ? "Stokta" : "Tükendi",
31 availabilityColor: product.isAvailable ? .green : .red,
32 categoryBadge: product.category.rawValue.capitalized
33 )
34 }
35 } catch {
36 self.error = mapError(error)
37 }
38 
39 isLoading = false
40 }
41 
42 // UI-specific formatting
43 private func formatPrice(_ price: Decimal) -> String {
44 let formatter = NumberFormatter()
45 formatter.numberStyle = .currency
46 formatter.currencyCode = "TRY"
47 return formatter.string(from: price as NSDecimalNumber) ?? "\(price) ₺"
48 }
49 
50 private func mapError(_ error: Error) -> String {
51 switch error {
52 case DomainError.outOfStock: return "Ürün stokta yok"
53 case is URLError: return "İnternet bağlantısı kontrol edin"
54 default: return "Beklenmeyen bir hata oluştu"
55 }
56 }
57}
58 
59// Presentation model - UI'a özel
60struct ProductDisplayItem: Identifiable {
61 let id: UUID
62 let name: String
63 let formattedPrice: String
64 let availability: String
65 let availabilityColor: Color
66 let categoryBadge: String
67}

Katman 4: Framework & Drivers (Infrastructure) {#framework-drivers}

En dıştaki katman. Veritabanı, network, UI framework gibi dış bağımlılıklar burada yaşar.

swift
1// Data/Repositories/ProductRepository.swift
2final class ProductRepository: ProductRepositoryProtocol {
3 private let apiClient: APIClientProtocol
4 private let cache: CacheProtocol
5 
6 init(apiClient: APIClientProtocol, cache: CacheProtocol) {
7 self.apiClient = apiClient
8 self.cache = cache
9 }
10 
11 func fetchAll() async throws -> [Product] {
12 // Önce cache'e bak
13 if let cached: [Product] = cache.get(key: "products") {
14 return cached
15 }
16 
17 // API'den çek
18 let response: ProductListResponse = try await apiClient.request(
19 endpoint: .products
20 )
21 
22 // DTO → Domain Entity dönüşümü
23 let products = response.items.map { dto in
24 Product(
25 id: dto.id,
26 name: dto.name,
27 description: dto.desc,
28 price: Decimal(dto.priceInCents) / 100,
29 stock: dto.stockCount,
30 category: Product.Category(rawValue: dto.category) ?? .electronics,
31 createdAt: dto.createdAt
32 )
33 }
34 
35 // Cache'e kaydet
36 cache.set(key: "products", value: products, expiry: .minutes(5))
37 
38 return products
39 }
40 
41 func fetch(id: UUID) async throws -> Product {
42 // Önce cache, sonra API...
43 let response: ProductDTO = try await apiClient.request(endpoint: .product(id: id))
44 return response.toDomain()
45 }
46}

Dependency Injection: Katmanları Birleştirmek {#dependency-injection}

Katmanları birbirine bağlamak için DI Container kullanıyoruz. Bu, tüm bağımlılıkların tek bir yerde yönetilmesini sağlar.

swift
1// App/DI/DependencyContainer.swift
2@MainActor
3final class DependencyContainer {
4 static let shared = DependencyContainer()
5 
6 // Infrastructure
7 private lazy var apiClient: APIClientProtocol = URLSessionAPIClient()
8 private lazy var cache: CacheProtocol = InMemoryCache()
9 
10 // Repositories
11 private lazy var productRepository: ProductRepositoryProtocol = {
12 ProductRepository(apiClient: apiClient, cache: cache)
13 }()
14 
15 // Use Cases
16 func makeFetchProductsUseCase() -> FetchProductsUseCaseProtocol {
17 FetchProductsUseCase(repository: productRepository)
18 }
19 
20 // ViewModels
21 func makeProductListViewModel() -> ProductListViewModel {
22 ProductListViewModel(fetchProductsUseCase: makeFetchProductsUseCase())
23 }
24}
25 
26// SwiftUI'da kullanım
27struct ProductListView: View {
28 @StateObject private var viewModel = DependencyContainer.shared.makeProductListViewModel()
29 
30 var body: some View {
31 List(viewModel.products) { item in
32 ProductRow(item: item)
33 }
34 .task { await viewModel.loadProducts() }
35 }
36}

SOLID Prensipleri Pratikte {#solid-prensipleri}

Clean Architecture, SOLID prensipleriyle el ele gider. Her bir prensibi iOS kontekstinde görelim:

Prensip
Açıklama
iOS Örneği
**Single Responsibility**
Her sınıf tek sorumluluk
ViewModel sadece UI state yönetir
**Open/Closed**
Genişlemeye açık, değişikliğe kapalı
Protocol + extension pattern
**Liskov Substitution**
Alt tipler üst tiplerin yerine geçebilmeli
Mock repository = gerçek repository
**Interface Segregation**
Gereksiz dependency olmamalı
Küçük, odaklı protokoller
**Dependency Inversion**
Soyutlamaya bağlan, somuta değil
Use Case → Protocol ← Repository

MVC vs MVVM vs VIPER vs Clean Architecture {#karsilastirma}

Kriter
MVC
MVVM
VIPER
Clean Architecture
**Karmaşıklık**
Düşük
Orta
Yüksek
Orta-Yüksek
**Test edilebilirlik**
Düşük
Orta
Yüksek
Çok Yüksek
**Ölçeklenebilirlik**
Düşük
Orta
Yüksek
Çok Yüksek
**Öğrenme eğrisi**
Kolay
Orta
Zor
Orta
**Boilerplate**
Az
Orta
Çok
Orta
**Framework bağımsızlığı**
Yok
Az
Kısmen
Tam
💡 Pro Tip: Küçük projeler (5-10 ekran) için MVVM yeterli. 20+ ekranlı, 3+ kişilik takımlarda Clean Architecture öne çıkar. VIPER'ın tüm avantajlarını daha az boilerplate ile sunar.

Testing Stratejisi {#testing}

Clean Architecture'ın en büyük avantajı: her katman bağımsız test edilebilir.

swift
1// Tests/UseCases/FetchProductsUseCaseTests.swift
2final class FetchProductsUseCaseTests: XCTestCase {
3 
4 func test_execute_returnsFilteredProducts() async throws {
5 // Arrange - Mock repository
6 let mockRepo = MockProductRepository()
7 mockRepo.stubbedProducts = [
8 Product(id: UUID(), name: "iPhone", description: "", price: 100, stock: 5, category: .electronics, createdAt: Date()),
9 Product(id: UUID(), name: "T-Shirt", description: "", price: 50, stock: 0, category: .clothing, createdAt: Date()),
10 ]
11 
12 let sut = FetchProductsUseCase(repository: mockRepo)
13 
14 // Act
15 let filter = ProductFilter(category: .electronics, inStockOnly: true)
16 let result = try await sut.execute(filter: filter)
17 
18 // Assert
19 XCTAssertEqual(result.count, 1)
20 XCTAssertEqual(result.first?.name, "iPhone")
21 }
22}
23 
24// Mock - tamamen kontrol edilebilir
25class MockProductRepository: ProductRepositoryProtocol {
26 var stubbedProducts: [Product] = []
27 var fetchAllCallCount = 0
28 
29 func fetchAll() async throws -> [Product] {
30 fetchAllCallCount += 1
31 return stubbedProducts
32 }
33 
34 func fetch(id: UUID) async throws -> Product {
35 guard let product = stubbedProducts.first(where: { $0.id == id }) else {
36 throw DomainError.notFound
37 }
38 return product
39 }
40}

Production Checklist {#production-checklist}

🔑 Bu Yazıdan Çıkarımlar

  1. Dependency Rule: Bağımlılıklar her zaman dıştan içe akar
  2. Domain katmanı pure Swift: Hiçbir Apple framework'ü import etme
  3. Her Use Case tek iş yapmalı: 50 satırı geçmesin
  4. Protocol-first design: Somut sınıflara değil, protokollere bağlan
  5. DI Container kullan: Bağımlılıkları merkezi yönet
  6. DTO ↔ Domain mapping: Katmanlar arası veri dönüşümü şart
  7. Test-first düşün: Her katmanı bağımsız test edebilmelisin

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?

Okuyucu Ödülü

Clean Architecture'ı bu derinlikte öğrenmek büyük bir başarı! Artık interview'larda mimari soruları rahatlıkla cevaplayabilir, mevcut projeleri refactor edebilir ve yeni projeleri sağlam temeller üzerine kurabilirsin. Sana özel hediye:

ALTIN İPUCU

Bu yazının en değerli bilgisi

Bu ipucu, yazının en önemli çıkarımını içeriyor.

Etiketler

#clean-architecture#solid#ios#architecture#design-patterns
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz