Azimjon Abdurasulov

15. Dependency Injection va DI Container: Inversion of Control

🏛️ iOS Design Patterns — Mukammal Enterprise & Architecture Qo‘llanma

Dependency Injection (DI) va Zamonaviy DI Container'lar

Dasturlashda eng ko‘p uchraydigan va kodni mo‘rt (fragile), testlab bo‘lmaydigan qiluvchi xato — bu obyektlarning o‘z ichida qaramliklarni (dependencies) init qilishi (tight coupling). Dependency Injection (DI) — bu arxitekturaning fundamental tamoyili bo‘lib, Inversion of Control (IoC) va SOLID'ning Dependency Inversion Principle (DIP) qoidasini amalda ta’minlaydi.

Ushbu darsda biz DI shakllari, Service Locator anti-patterni xavflari, Swift 6 da Sendable va actor-safe bo‘lgan yengil DI Container hamda @Injected Property Wrapper arxitekturasini quramiz.


1. Tight Coupling vs Inversion of Control (IoC)

Obyekt o‘ziga kerakli xizmatni o‘zi yaratsa:

// ❌ ANTI-PATTERN: Tight Coupling
final class ProfileViewModel {
    private let apiService = NetworkService() // Qattiq bog'langan!
    private let analytics = FirebaseAnalytics()
    // Testda NetworkService o'rniga Mock berib bo'lmaydi!
}

Buning o‘rniga, obyekt o‘z qaramliklarini tashqaridan qabul qilishi shart:

graph TD
    subgraph "Tight Coupling (O'tmish)"
        A[ProfileViewModel] -->|To'g'ridan-to'g'ri yaratadi| B[NetworkService]
        A -->|To'g'ridan-to'g'ri yaratadi| C[FirebaseAnalytics]
    end

    subgraph "Inversion of Control (DI)"
        VM[ProfileViewModel] -->|Faqat protokollarga bog'liq| P1[NetworkServiceProtocol]
        VM -->|Faqat protokollarga bog'liq| P2[AnalyticsServiceProtocol]
        DI[DI Container / Assembler] -.->|Injects Real Impl| P1
        DI -.->|Injects Mock Impl| P2
    end

2. Dependency Injection Shakllari

iOS'da DI ning 3 xil usuli mavjud:

UsulAfzalligiKamchiligiTavsiya
Constructor (Initializer) Injection100% Compile-time xavfsiz, immutability (let), qaramliksiz obyekt yaratilmaydiInit parametrlar ko‘payib ketishi mumkin⭐ Oltin standart (Default)
Property InjectionStoryboard / UIKit lifecycle yoki ixtiyoriy pluginlar uchun qulayMutable (var) bo‘lishi shart, nil xavfi borFaqat shart bo‘lganda
Method InjectionBir martalik parametr (masalan, formatter yoki logger) uzatishDoimiy servislar uchun noqulayYordamchi amallarga

Constructor Injection Namunasi (Swift 6)

protocol NetworkServiceProtocol: Sendable {
    func fetchUserProfile(id: String) async throws -> UserProfile
}

protocol AnalyticsServiceProtocol: Sendable {
    func track(event: String)
}

@MainActor
final class ProfileViewModel {
    // Immutability: tashqaridan beriladi va qayta o'zgarmaydi
    private let networkService: any NetworkServiceProtocol
    private let analytics: any AnalyticsServiceProtocol
    
    // Constructor Injection
    init(
        networkService: any NetworkServiceProtocol,
        analytics: any AnalyticsServiceProtocol
    ) {
        self.networkService = networkService
        self.analytics = analytics
    }
    
    func loadUser(id: String) async {
        do {
            let profile = try await networkService.fetchUserProfile(id: id)
            analytics.track(event: "profile_loaded_success")
            // Update UI state
        } catch {
            analytics.track(event: "profile_load_failed")
        }
    }
}

3. Service Locator — Nega bu Anti-Pattern?

Ko‘plab jamoalar DI o‘rniga static ServiceLocator yaratishadi:

// ⚠️ XAVFLI: Service Locator Anti-Pattern
final class ServiceLocator {
    static let shared = ServiceLocator()
    private var services: [String: Any] = [:]
    
    func register<T>(_ service: T) {
        services[String(describing: T.self)] = service
    }
    
    func resolve<T>() -> T {
        guard let service = services[String(describing: T.self)] as? T else {
            fatalError("Servis topilmadi! Runtime Crash!")
        }
        return service
    }
}

Nega Service Locator xavfli?

  1. Yashirin Qaramliklar (Hidden Dependencies): Sinf tashqaridan qaraganda hech qanday parametr so‘ramaydi (init()). Ammo ichida ServiceLocator.shared.resolve() chaqiriladi. Kodni o‘qigan dasturchi bu sinf ishlashi uchun qaysi servislar registratsiya qilingan bo‘lishi kerakligini bilmaydi.
  2. Runtime Crash: Agar bitta servis unutilgan bo‘lsa, kod compile bo‘ladi, lekin dastur ishga tushganda fatalError beradi.
  3. Temporal Coupling: Obyektlar yaratilish tartibiga qattiq bog‘lanib qoladi.

4. Swift 6 Xavfsiz, Yengil DI Container va @Injected Property Wrapper

Keling, zamonaviy SwiftUI va Swift Concurrency talablariga to‘liq javob beradigan, Sendable va type-safe Lightweight DI Container quramiz.

sequenceDiagram
    autonumber
    participant App as App Launch / Assembler
    participant Container as AppContainer (Actor)
    participant VM as HomeViewModel
    participant Network as LiveNetworkService

    App->>Container: register(LiveNetworkService.self)
    Note over Container: Servislar Type bo'yicha saqlanadi
    VM->>Container: resolve(NetworkServiceProtocol.self)
    Container-->>VM: return LiveNetworkService instance
    VM->>Network: fetchData()

Swift 6 Real Implementatsiyasi:

import Foundation

// 1. Thread-safe DI Container
public final class DIContainer: @unchecked Sendable {
    public static let shared = DIContainer()
    
    private let lock = NSLock()
    private var factories: [String: () -> Any] = [:]
    private var singletons: [String: Any] = [:]
    
    public init() {}
    
    // Yangi factory registratsiyasi (Har safar yangi instance)
    public func register<T>(
        _ type: T.Type,
        factory: @escaping () -> T
    ) {
        lock.lock()
        defer { lock.unlock() }
        let key = String(reflecting: type)
        factories[key] = factory
    }
    
    // Singleton registratsiyasi (Bitta doimiy xotiradagi instance)
    public func registerSingleton<T>(
        _ type: T.Type,
        instance: T
    ) {
        lock.lock()
        defer { lock.unlock() }
        let key = String(reflecting: type)
        singletons[key] = instance
    }
    
    // Servisni qabul qilish
    public func resolve<T>(_ type: T.Type = T.self) -> T {
        lock.lock()
        defer { lock.unlock() }
        let key = String(reflecting: type)
        
        // 1. Singleton tekshiruvi
        if let instance = singletons[key] as? T {
            return instance
        }
        
        // 2. Factory tekshiruvi
        if let factory = factories[key], let instance = factory() as? T {
            return instance
        }
        
        fatalError("DIContainer: \(key) turi uchun servis registratsiya qilinmagan!")
    }
}

// 2. Ergonomik @Injected Property Wrapper
@propertyWrapper
public struct Injected<Service> {
    private var service: Service
    
    public init(container: DIContainer = .shared) {
        self.service = container.resolve(Service.self)
    }
    
    public var wrappedValue: Service {
        get { service }
        mutating set { service = newValue }
    }
}

5. SwiftUI va XCTest'da Foydalanish

App Bootstrap:

func configureDependencies() {
    let container = DIContainer.shared
    
    container.registerSingleton(NetworkServiceProtocol.self, instance: LiveNetworkService())
    container.registerSingleton(AnalyticsServiceProtocol.self, instance: MixpanelAnalyticsService())
}

SwiftUI ViewModel'da Ishlatish:

import SwiftUI

@Observable
@MainActor
final class HomeViewModel {
    @Injected private var network: NetworkServiceProtocol
    @Injected private var analytics: AnalyticsServiceProtocol
    
    var items: [String] = []
    
    func load() async {
        analytics.track(event: "home_screen_open")
        // Network orqali yuklash...
    }
}

XCTest'da 100% Izolatsiyalangan Unit Test:

import XCTest

final class MockNetworkService: NetworkServiceProtocol {
    var stubbedProfile: UserProfile?
    var didCallFetch = false
    
    func fetchUserProfile(id: String) async throws -> UserProfile {
        didCallFetch = true
        if let profile = stubbedProfile {
            return profile
        }
        throw URLError(.badServerResponse)
    }
}

final class ProfileViewModelTests: XCTestCase {
    func test_loadUser_success() async {
        // DI Container'siz ham Constructor orqali to'g'ridan-to'g'ri mock berish eng toza yo'l:
        let mockNetwork = MockNetworkService()
        mockNetwork.stubbedProfile = UserProfile(id: "123", name: "Azimjon")
        let mockAnalytics = MockAnalyticsService()
        
        let sut = ProfileViewModel(networkService: mockNetwork, analytics: mockAnalytics)
        
        await sut.loadUser(id: "123")
        
        XCTAssertTrue(mockNetwork.didCallFetch)
        XCTAssertEqual(mockAnalytics.events, ["profile_loaded_success"])
    }
}

6. Senior & Staff Xulosa

  1. Over-engineering qilmang: Agar ilovangiz o‘rtacha hajmda bo‘lsa, Constructor Injection + oddiy Factory / Coordinator barcha ehtiyojingizni qoplaydi. 10 xil uchinchi tomon DI kutubxonalarini (Swinject, Needle, Factory) yuklab loyihani og‘irlashtirish shart emas.
  2. SwiftUI Environment: SwiftUI ning @Environment va @EnvironmentObject lari View darajasidagi Dependency Injection hisoblanadi. Ammo biznes-mantiq (ViewModel, Domain Interactor) darajasida Sof Swift DI usullari qo‘llanilishi maqsadga muvofiq.
  3. Property Wrapper ehtiyoti: @Injected wrapper'laridan faqat arxitektura ruxsat bergan cheklangan qatlamlarda (masalan, View yoki Coordinator) foydalaning, toza biznes-mantiq sinflarida esa doimo Constructor Injectionni tanlang!

Bilimni Mustahkamlash uchun Savollar

<details> <summary><b>1. Nega Constructor Injection Property Injection'dan ustun hisoblanadi?</b></summary> Constructor Injection ob’yektning barcha zaruriy qaramliklarini yaratilish paytidayoq (init) talab qiladi. Bu let (immutability) imkonini beradi va ob’yektning chala yoki yaroqsiz (nil) holatda yaratilishini compile vaqtidayoq taqiqlaydi. </details>

<details> <summary><b>2. Service Locator va Dependency Injection o'rtasidagi asosiy farq nima?</b></summary> DI da ob’yekt o'z qaramliklaridan bexabar bo‘ladi — ular unga tashqaridan beriladi (passiv qabul qiluvchi). Service Locator'da esa ob’yekt global reyestrga murojaat qilib, o‘zi qidirib topadi (faol qidiruvchi). Bu esa qaramliklarni yashiradi va testlashni murakkablashtiradi. </details>

Markdown manba

iOS Lessons