Azimjon Abdurasulov

02. Singleton: Haqiqat, Data Race va Unit Test Tuzoqlari

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

02. Creational: Singleton — Haqiqat, Qotil va Yechim

"Singleton — bu junior dasturchining eng sevimli quroli, chunki u har qanday ma'lumotni istalgan joydan olish imkonini beradi. Ammo aynan shu qurol 6 oydan keyin butun loyihani test qilib bo'lmaydigan, race conditionlarga to'la botqoqqa aylantiradi."


1. Singleton Nima va U Qanday Ishlaydi?

Singleton — bu dasturning butun ishlash davomida (Lifecycle) sinfning faqat bitta nusxasi (instance) mavjud bo'lishini va unga global kirish nuqtasi (shared) taqdim etilishini kafolatlovchi pattern.

Klassik Swift Implementatsiyasi:

final class AnalyticsManager {
    // 1. Yagona global nusxa (Swift'da static let doimo thread-safe va lazy ishlaydi)
    static let shared = AnalyticsManager()
    
    // 2. Tashqaridan yangi nusxa ochishni TAQIQLASH shart!
    private init() {}
    
    func track(event: String) {
        print("Tadbir qayd etildi: \(event)")
    }
}

2. Apple Ekotizimidagi Singletonlar: "True" vs "Default"

Apple o'zining Cocoa va Foundation freymvorklarida Singleton patternidan keng foydalanadi:

  • UIApplication.shared — haqiqiy (True) Singleton. Yangi UIApplication() ochish taqiqlangan.
  • FileManager.default — Default Singleton. FileManager.default mavjud, biroq maxsus kesh papkalari uchun alohida FileManager() ochishingiz ham mumkin.
  • UserDefaults.standard — Default Singleton.
  • URLSession.shared — Default Singleton (o'z fon sessiyangiz uchun URLSession(configuration:) qura olasiz).

3. Singleton Nega "Anti-Pattern"ga Aylanadi? 4 Ta Halokatli Tuzoq

flowchart TD
    subgraph S1["Singletonning 4 Yashirin Qotili"]
        direction TB
        K1["1. Yashirin Bog'liqliklar (Hidden Dependencies)\nFunksiya nimaga bog'liqligi imzosida (Signature) ko'rinmaydi"]
        K2["2. Global Mutable State\nIstalgan View xohlagan paytda qiymatni o'zgartirib yuborishi mumkin"]
        K3["3. Unit Testlarning O'limi (Test Pollution)\n1-test o'zgartirgan state 2-test natijasini buzadi"]
        K4["4. Swift 6 Concurrency Race Conditions\nKo'p oqimli muhitda crash yoki data corruption"]
    end

    style K1 fill:#251014,stroke:#f87171,stroke-width:1px
    style K2 fill:#251014,stroke:#f87171,stroke-width:1px
    style K3 fill:#251014,stroke:#f87171,stroke-width:1px
    style K4 fill:#251014,stroke:#f87171,stroke-width:1px

1. Yashirin Bog'liqlik (Hidden Dependency) Fojiasi:

// ❌ YOMON: Funksiya tashqi dunyoga nima kerakligini yashirmoqda
func processOrder(order: Order) {
    // Bu funksiya AuthService, DatabaseManager va NetworkManager'ga
    // yashirin bog'langanini faqat ichki kodni o'qib bilish mumkin!
    let token = AuthService.shared.token
    DatabaseManager.shared.save(order)
    NetworkManager.shared.send(order, token: token)
}

2. Unit Test Falaji (State Pollution):

Tasavvur qiling, sizda UserSession.shared.isLoggedIn bor:

  • Test 1: Login jarayonini tekshiradi va isLoggedIn = true qiladi.
  • Test 2: Chiqish (Logout) jarayonini tekshirmoqchi. Lekin Test 1 dan qolgan isLoggedIn = true holati sababli Test 2 kutilmaganda fail bo'ladi!
  • Singleton testlar o'rtasida xotiradan tozalanmaydi, testlarni alohida (isolated) yurgizib bo'lmaydi.

4. Swift 6 Concurrency da Singleton: Thread Safety

Swift 5 da ko'pchilik dasturchilar Singleton ichidagi o'zgaruvchilarni himoyasiz qoldirishar edi, bu ko'p oqimli (Multi-threaded) muhitda crash keltirib chiqarardi:

sequenceDiagram
    autonumber
    participant ThreadA as Thread A (Background)
    participant Singleton as SessionManager.shared
    participant ThreadB as Thread B (Main)

    ThreadA->>Singleton: session.token = "new_token" (Yozmoqda...)
    ThreadB->>Singleton: print(session.token) (O'qimoqda...)
    Singleton-->>Singleton: 💥 DATA RACE! Crash (EXC_BAD_ACCESS)!

Swift 6 Yechimi: Global Actor Singleton

Swift 6 Strict Concurrency standartida Singleton'ni actor sifatida e'lon qilish ma'lumotlar poygasi (Data Race) ni kompilyatsiya bosqichidayoq to'liq bartaraf qiladi:

/// Swift 6 Concurrency uchun 100% xavfsiz Actor Singleton
public actor SecureSessionManager {
    public static let shared = SecureSessionManager()
    
    private var authToken: String?
    
    private init() {}
    
    public func updateToken(_ token: String) {
        self.authToken = token
    }
    
    public func getToken() -> String? {
        return self.authToken
    }
}

5. Senior Yechim: Singleton'ni Protocol va DI Bilan Zararsizlantirish

Singleton'dan butunlay voz kechish shart emas, uni Dependency Injection (DI) orqali boshqariladigan qilish kerak:

flowchart LR
    Protocol["protocol AnalyticsTracking (Kontrakt)"]
    Service["RealAnalyticsService.shared"] -.->|"Tasdiqlaydi"| Protocol
    Mock["MockAnalyticsService (Testlar uchun)"] -.->|"Tasdiqlaydi"| Protocol

    ViewModel["CheckoutViewModel"] -->|"faqat protocolga bog'langan"| Protocol

    style Protocol fill:#131720,stroke:#bef264,stroke-width:2px
    style ViewModel fill:#181d26,stroke:#7da2ce,stroke-width:1px

Kod Misoli:

// 1. Protokol ajratish
public protocol AnalyticsTracking: Sendable {
    func track(event: String)
}

// 2. Haqiqiy Singleton (Protokolni tasdiqlaydi)
public final class ProductionAnalyticsService: AnalyticsTracking {
    public static let shared = ProductionAnalyticsService()
    private init() {}
    
    public func track(event: String) {
        // Firebase / Amplitude API chaqiruvi
    }
}

// 3. ViewModel: Singleton'ga to'g'ridan-to'g'ri bog'lanmaydi!
public final class CheckoutViewModel {
    private let analytics: any AnalyticsTracking
    
    // Default qiymat sifatida Singleton beriladi,
    // ammo testda istalgan Mock'ni uzatish mumkin!
    public init(analytics: any AnalyticsTracking = ProductionAnalyticsService.shared) {
        self.analytics = analytics
    }
    
    public func purchaseCompleted() {
        analytics.track(event: "purchase_success")
    }
}

// 4. Unit Test uchun Mock:
#if DEBUG
final class MockAnalyticsService: AnalyticsTracking {
    var trackedEvents: [String] = []
    func track(event: String) {
        trackedEvents.append(event)
    }
}
#endif

Endi CheckoutViewModel 100% testable bo'ldi! Test paytida haqiqiy Firebase'ga so'rov ketmaydi va xotira ifloslanmaydi.


6. Interaktiv Savol-Javob (Self-Check Quiz)

<details> <summary><b>1-Savol: Nega Swift'da 'static let shared = MyClass()' avtomatik tarzda thread-safe hisoblanadi?</b> (Yechimni ko'rish uchun bosing)</summary>

Tahlil va Yechim: Swift kompilyatori global va statik o'zgaruvchilarni ichki darajada pthread_once (yoki dispatch_once) orqali o'rab qo'yadi. Agar bir vaqtning o'zida 10 ta turli oqim (Thread) MyClass.shared ga murojaat qilsa ham, Swift faqat 1 ta birinchi oqimga obyektni xotirada yaratishga ruxsat beradi, qolgan 9 tasi esa bloklanib, tayyor obyektni qabul qiladi. </details>

<details> <summary><b>2-Savol: Qaysi holatda Singleton xotira oqishiga (Memory Leak / Retain Cycle) sabab bo'ladi?</b> (Yechimni ko'rish uchun bosing)</summary>

Tahlil va Yechim: Singleton ilova yopilguncha xotirada (RAM) doimiy yashaydi. Agar siz Singleton ichidagi obunachilar (Observers yoki Delegates) massiviga strong havola bilan biror UIViewController yoki Viewni saqlab qo'ysangiz, o'sha ViewController ekrandan yopilsa ham xotiradan o'chmaydi va butun umrga xotirada qolib ketadi (Retain Cycle). Yechim: Delegatlar va obunachilar doimo weak bo'lishi shart! </details>


7. Amaliy Topshiriq (Senior Challenge)

Loyihangizdagi mavjud Singletonlardan birini (masalan NetworkManager.shared) oling:

  1. Unga mos keluvchi NetworkManaging protokolini yozing.
  2. Unga bog'langan ViewModel'ning init metodini Constructor Injection'ga o'tkazing (init(network: any NetworkManaging = NetworkManager.shared)).
  3. Ushbu ViewModel uchun MockNetworkManager orqali muvaffaqiyatli ma'lumot kelishi va xatolik (404/500) kelishi holatlarini tekshiruvchi 2 ta Unit Test yozing!

Markdown manba

iOS Lessons