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. YangiUIApplication()ochish taqiqlangan.FileManager.default— Default Singleton.FileManager.defaultmavjud, biroq maxsus kesh papkalari uchun alohidaFileManager()ochishingiz ham mumkin.UserDefaults.standard— Default Singleton.URLSession.shared— Default Singleton (o'z fon sessiyangiz uchunURLSession(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 = trueqiladi. - Test 2: Chiqish (Logout) jarayonini tekshirmoqchi. Lekin Test 1 dan qolgan
isLoggedIn = trueholati 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:
- Unga mos keluvchi
NetworkManagingprotokolini yozing. - Unga bog'langan ViewModel'ning
initmetodini Constructor Injection'ga o'tkazing (init(network: any NetworkManaging = NetworkManager.shared)). - Ushbu ViewModel uchun
MockNetworkManagerorqali muvaffaqiyatli ma'lumot kelishi va xatolik (404/500) kelishi holatlarini tekshiruvchi 2 ta Unit Test yozing!