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

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

iOS uygulamalarında Dependency Injection stratejilerini öğrenin. Constructor, property ve method injection, DI container, protocol-based DI ve testing ile DI.

Swift'te Dependency Injection: Container'dan Property Wrapper'a

"Bu class'ı test etmek imkansız!" — Eğer bu cümleyi söylediysen, muhtemelen dependency injection kullanmıyorsun. DI, SOLID prensiplerinin 'D'si olan Dependency Inversion Principle'ın pratiğe dökülmüş halidir. Doğru uygulandığında, kodun test edilebilir, sürdürülebilir ve modüler olur.

💡 Hızlı Not: Bu rehber Robert C. Martin'in Clean Architecture prensipleri ve iOS community'deki popüler DI framework'lerinden (Swinject, Factory, swift-dependencies) derlendi.

İçindekiler

  1. DI Nedir ve Neden Önemli?
  2. Constructor Injection
  3. Property Injection
  4. Method Injection
  5. Protocol-Based DI
  6. DI Container Oluşturma
  7. SwiftUI Environment ile DI
  8. Service Locator Anti-Pattern
  9. Testing ile DI
  10. Production Strategies

DI Nedir ve Neden Önemli? {#di-nedir}

Dependency Injection, bir nesnenin bağımlılıklarını dışarıdan alması demektir. Karşılaştırma:

swift
1// ❌ Tight coupling - bağımlılık içeride oluşturuluyor
2class UserViewModel {
3 private let repository = UserRepository() // Hardcoded!
4 private let analytics = FirebaseAnalytics() // Hardcoded!
5 
6 func loadUser() async { /* ... */ }
7}
8 
9// ✅ Dependency Injection - bağımlılıklar dışarıdan veriliyor
10class UserViewModel {
11 private let repository: UserRepositoryProtocol
12 private let analytics: AnalyticsProtocol
13 
14 init(repository: UserRepositoryProtocol, analytics: AnalyticsProtocol) {
15 self.repository = repository
16 self.analytics = analytics
17 }
18 
19 func loadUser() async { /* ... */ }
20}

DI'ın Faydaları

Fayda
Açıklama
Test edilebilirlik
Mock/stub ile birim test
Modülerlik
Module'ler birbirinden bağımsız
Esneklik
Runtime'da implementation değiştir
Okunabilirlik
Bağımlılıklar açıkça görünür
Yeniden kullanılabilirlik
Farklı context'lerde aynı class

Constructor Injection {#constructor-injection}

En yaygın ve önerilen DI türü — bağımlılıklar init ile verilir:

swift
1protocol NetworkServiceProtocol {
2 func fetch<T: Decodable>(from url: URL) async throws -> T
3}
4 
5protocol StorageServiceProtocol {
6 func save<T: Encodable>(_ value: T, forKey key: String) throws
7 func load<T: Decodable>(forKey key: String) throws -> T?
8}
9 
10class ProductViewModel: ObservableObject {
11 @Published var products: [Product] = []
12 @Published var isLoading = false
13 @Published var error: String?
14 
15 private let networkService: NetworkServiceProtocol
16 private let storageService: StorageServiceProtocol
17 
18 // Constructor injection - tüm bağımlılıklar init'te
19 init(
20 networkService: NetworkServiceProtocol,
21 storageService: StorageServiceProtocol
22 ) {
23 self.networkService = networkService
24 self.storageService = storageService
25 }
26 
27 func loadProducts() async {
28 isLoading = true
29 defer { isLoading = false }
30 
31 do {
32 // Önce cache'den dene
33 if let cached: [Product] = try storageService.load(forKey: "products") {
34 products = cached
35 }
36 // Sonra network'ten güncelle
37 let remote: [Product] = try await networkService.fetch(from: productsURL)
38 products = remote
39 try storageService.save(remote, forKey: "products")
40 } catch {
41 self.error = error.localizedDescription
42 }
43 }
44}

Property Injection {#property-injection}

Optional bağımlılıklar veya sonradan set edilen bağımlılıklar için:

swift
1class ImageLoader {
2 // Property injection - sonradan set edilebilir
3 var cache: ImageCacheProtocol = InMemoryImageCache()
4 var transformer: ImageTransformerProtocol?
5 
6 func loadImage(from url: URL) async throws -> UIImage {
7 if let cached = cache.get(for: url) { return cached }
8 
9 let (data, _) = try await URLSession.shared.data(from: url)
10 var image = UIImage(data: data)!
11 
12 if let transformer {
13 image = transformer.transform(image)
14 }
15 
16 cache.set(image, for: url)
17 return image
18 }
19}

Method Injection {#method-injection}

Bağımlılık sadece tek bir fonksiyon çağrısında gerektiğinde:

swift
1class ReportGenerator {
2 func generate(
3 data: [SalesData],
4 formatter: ReportFormatterProtocol, // Method injection
5 exporter: ReportExporterProtocol // Method injection
6 ) throws -> URL {
7 let formatted = formatter.format(data)
8 return try exporter.export(formatted)
9 }
10}
11 
12// Her çağrıda farklı strateji
13let pdfURL = try generator.generate(
14 data: salesData,
15 formatter: TableFormatter(),
16 exporter: PDFExporter()
17)
18 
19let csvURL = try generator.generate(
20 data: salesData,
21 formatter: CSVFormatter(),
22 exporter: FileExporter(format: .csv)
23)

Protocol-Based DI {#protocol-based}

iOS'ta DI'ın temeli protocol'lerdir:

swift
1// 1. Protocol tanımla
2protocol AuthServiceProtocol {
3 func login(email: String, password: String) async throws -> User
4 func logout() async throws
5 var currentUser: User? { get }
6}
7 
8// 2. Live implementation
9class FirebaseAuthService: AuthServiceProtocol {
10 func login(email: String, password: String) async throws -> User {
11 let result = try await Auth.auth().signIn(withEmail: email, password: password)
12 return User(firebaseUser: result.user)
13 }
14 func logout() async throws { try Auth.auth().signOut() }
15 var currentUser: User? { Auth.auth().currentUser.map(User.init) }
16}
17 
18// 3. Mock implementation
19class MockAuthService: AuthServiceProtocol {
20 var loginResult: Result<User, Error> = .success(User.mock)
21 var logoutCalled = false
22 var currentUser: User? = User.mock
23 
24 func login(email: String, password: String) async throws -> User {
25 try loginResult.get()
26 }
27 func logout() async throws { logoutCalled = true; currentUser = nil }
28}

DI Container Oluşturma {#di-container}

swift
1// Basit DI Container
2final class DIContainer {
3 static let shared = DIContainer()
4 
5 private var factories: [String: () -> Any] = [:]
6 
7 func register<T>(_ type: T.Type, factory: @escaping() -> T) {
8 let key = String(describing: type)
9 factories[key] = factory
10 }
11 
12 func resolve<T>(_ type: T.Type) -> T {
13 let key = String(describing: type)
14 guard let factory = factories[key] else {
15 fatalError("No registration for \(key)")
16 }
17 return factory() as! T
18 }
19}
20 
21// Registration
22let container = DIContainer.shared
23container.register(NetworkServiceProtocol.self) { URLSessionNetworkService() }
24container.register(StorageServiceProtocol.self) { UserDefaultsStorage() }
25container.register(AuthServiceProtocol.self) { FirebaseAuthService() }
26 
27// Resolution
28let viewModel = ProductViewModel(
29 networkService: container.resolve(NetworkServiceProtocol.self),
30 storageService: container.resolve(StorageServiceProtocol.self)
31)

SwiftUI Environment ile DI {#swiftui-environment}

SwiftUI'ın @Environment mekanizması aslında bir DI sistemidir:

swift
1// Custom environment key
2struct AuthServiceKey: EnvironmentKey {
3 static let defaultValue: AuthServiceProtocol = FirebaseAuthService()
4}
5 
6extension EnvironmentValues {
7 var authService: AuthServiceProtocol {
8 get { self[AuthServiceKey.self] }
9 set { self[AuthServiceKey.self] = newValue }
10 }
11}
12 
13// View'da kullanım
14struct LoginView: View {
15 @Environment(\.authService) var authService
16 
17 var body: some View { /* ... */ }
18}
19 
20// Test/Preview'da override
21LoginView()
22 .environment(\.authService, MockAuthService())

Service Locator Anti-Pattern {#service-locator}

Service Locator, DI'a alternatif gibi görünse de anti-pattern'dir:

swift
1// ❌ Service Locator - bağımlılıklar gizli
2class OrderViewModel {
3 func placeOrder() {
4 // Bağımlılık gizli - test edilmesi zor
5 let service = ServiceLocator.resolve(OrderServiceProtocol.self)
6 let analytics = ServiceLocator.resolve(AnalyticsProtocol.self)
7 }
8}
9 
10// ✅ Constructor Injection - bağımlılıklar açık
11class OrderViewModel {
12 private let orderService: OrderServiceProtocol
13 private let analytics: AnalyticsProtocol
14 
15 init(orderService: OrderServiceProtocol, analytics: AnalyticsProtocol) {
16 self.orderService = orderService
17 self.analytics = analytics
18 }
19}

Testing ile DI {#testing-di}

swift
1class ProductViewModelTests: XCTestCase {
2 func testLoadProducts_success() async {
3 // Arrange - Mock injection
4 let mockNetwork = MockNetworkService()
5 mockNetwork.result = [Product(name: "Test", price: 9.99)]
6 
7 let mockStorage = MockStorageService()
8 
9 let viewModel = ProductViewModel(
10 networkService: mockNetwork,
11 storageService: mockStorage
12 )
13 
14 // Act
15 await viewModel.loadProducts()
16 
17 // Assert
18 XCTAssertEqual(viewModel.products.count, 1)
19 XCTAssertEqual(viewModel.products.first?.name, "Test")
20 XCTAssertFalse(viewModel.isLoading)
21 XCTAssertNil(viewModel.error)
22 }
23 
24 func testLoadProducts_networkError_showsCached() async {
25 let mockNetwork = MockNetworkService()
26 mockNetwork.error = URLError(.notConnectedToInternet)
27 
28 let mockStorage = MockStorageService()
29 mockStorage.data["products"] = [Product(name: "Cached")]
30 
31 let viewModel = ProductViewModel(
32 networkService: mockNetwork,
33 storageService: mockStorage
34 )
35 
36 await viewModel.loadProducts()
37 
38 XCTAssertEqual(viewModel.products.first?.name, "Cached")
39 }
40}

Production Strategies {#production}

  1. Constructor injection öncelikli tercih et
  2. Protocol ile soyutla, concrete type'a bağımlılık yaratma
  3. Composition Root — DI registration tek noktada (AppDelegate/App)
  4. Property wrapper ile kolaylık sağla (advanced)
  5. Scope yönetimi — singleton vs transient vs scoped

Easter Egg

Gizli bir bilgi buldun!

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

ALTIN İPUCU

Bu yazının en değerli bilgisi

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

Okuyucu Ödülü

Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var: **Kaynaklar:** - [Martin Fowler: Inversion of Control Containers](https://martinfowler.com/articles/injection.html) - [Factory - Swift DI Framework](https://github.com/hmlongco/Factory) - [swift-dependencies by Point-Free](https://github.com/pointfreeco/swift-dependencies)

Etiketler

#dependency-injection#di#swift#architecture#testing#solid#inversion-of-control
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