"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
- DI Nedir ve Neden Önemli?
- Constructor Injection
- Property Injection
- Method Injection
- Protocol-Based DI
- DI Container Oluşturma
- SwiftUI Environment ile DI
- Service Locator Anti-Pattern
- Testing ile DI
- 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şturuluyor2class 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 veriliyor10class UserViewModel {11 private let repository: UserRepositoryProtocol12 private let analytics: AnalyticsProtocol13 14 init(repository: UserRepositoryProtocol, analytics: AnalyticsProtocol) {15 self.repository = repository16 self.analytics = analytics17 }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 -> T3}4 5protocol StorageServiceProtocol {6 func save<T: Encodable>(_ value: T, forKey key: String) throws7 func load<T: Decodable>(forKey key: String) throws -> T?8}9 10class ProductViewModel: ObservableObject {11 @Published var products: [Product] = []12 @Published var isLoading = false13 @Published var error: String?14 15 private let networkService: NetworkServiceProtocol16 private let storageService: StorageServiceProtocol17 18 // Constructor injection - tüm bağımlılıklar init'te19 init(20 networkService: NetworkServiceProtocol,21 storageService: StorageServiceProtocol22 ) {23 self.networkService = networkService24 self.storageService = storageService25 }26 27 func loadProducts() async {28 isLoading = true29 defer { isLoading = false }30 31 do {32 // Önce cache'den dene33 if let cached: [Product] = try storageService.load(forKey: "products") {34 products = cached35 }36 // Sonra network'ten güncelle37 let remote: [Product] = try await networkService.fetch(from: productsURL)38 products = remote39 try storageService.save(remote, forKey: "products")40 } catch {41 self.error = error.localizedDescription42 }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 edilebilir3 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 image18 }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 injection5 exporter: ReportExporterProtocol // Method injection6 ) throws -> URL {7 let formatted = formatter.format(data)8 return try exporter.export(formatted)9 }10}11 12// Her çağrıda farklı strateji13let 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ımla2protocol AuthServiceProtocol {3 func login(email: String, password: String) async throws -> User4 func logout() async throws5 var currentUser: User? { get }6}7 8// 2. Live implementation9class 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 implementation19class MockAuthService: AuthServiceProtocol {20 var loginResult: Result<User, Error> = .success(User.mock)21 var logoutCalled = false22 var currentUser: User? = User.mock23 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 Container2final 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] = factory10 }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! T18 }19}20 21// Registration22let container = DIContainer.shared23container.register(NetworkServiceProtocol.self) { URLSessionNetworkService() }24container.register(StorageServiceProtocol.self) { UserDefaultsStorage() }25container.register(AuthServiceProtocol.self) { FirebaseAuthService() }26 27// Resolution28let 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 key2struct 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ım14struct LoginView: View {15 @Environment(\.authService) var authService16 17 var body: some View { /* ... */ }18}19 20// Test/Preview'da override21LoginView()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 gizli2class OrderViewModel {3 func placeOrder() {4 // Bağımlılık gizli - test edilmesi zor5 let service = ServiceLocator.resolve(OrderServiceProtocol.self)6 let analytics = ServiceLocator.resolve(AnalyticsProtocol.self)7 }8}9 10// ✅ Constructor Injection - bağımlılıklar açık11class OrderViewModel {12 private let orderService: OrderServiceProtocol13 private let analytics: AnalyticsProtocol14 15 init(orderService: OrderServiceProtocol, analytics: AnalyticsProtocol) {16 self.orderService = orderService17 self.analytics = analytics18 }19}Testing ile DI {#testing-di}
swift
1class ProductViewModelTests: XCTestCase {2 func testLoadProducts_success() async {3 // Arrange - Mock injection4 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: mockStorage12 )13 14 // Act15 await viewModel.loadProducts()16 17 // Assert18 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: mockStorage34 )35 36 await viewModel.loadProducts()37 38 XCTAssertEqual(viewModel.products.first?.name, "Cached")39 }40}Production Strategies {#production}
- Constructor injection öncelikli tercih et
- Protocol ile soyutla, concrete type'a bağımlılık yaratma
- Composition Root — DI registration tek noktada (AppDelegate/App)
- Property wrapper ile kolaylık sağla (advanced)
- 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)

