Singleton — създаващ шаблон, който гарантира единичен екземпляр на клас и предоставя глобална точка за достъп до него. Singleton се използва широко в мобилното разработване за споделени ресурси: мрежови клиенти, бази данни, мениджъри на настройки. Шаблонът е описан в класическата книга GoF (1994) и остава един от най-разпознаваемите. Повече — на Refactoring Guru: Singleton.
Основно
Singleton — създаващ шаблон, описан от GoF (Gang of Four) през 1994 г. Шаблонът решава две задачи: ограничава създаването на екземпляр на клас до един обект и предоставя глобален достъп до този обект. Singleton е полезен за ресурси, които трябва да бъдат уникални: фабрика за сесии, кеш за изображения, мениджър за връзка с база данни, клиент Crashlytics или Analytics.
Имплементация на Singleton изисква частен конструктор (забранява външно създаване), статично поле с единичен екземпляр и статичен метод за достъп (shared, instance, getInstance). Клиентите викат Singleton.shared.method(), без да се грижат за създаването на обект. Шаблонът е популярен в iOS и Android: URLSession.shared, UserDefaults.standard, FirebaseApp.sharedInstance — всичко това са Singleton. Прекомерната употреба на Singleton обаче води до анти-шаблон Global State.
Проблеми на Singleton — скрити зависимости (класовете имплицитно зависят от обекта Singleton), трудност при тестване (не може да се замени екземпляр в тест без допълнителни усилия), нарушаване на Single Responsibility Principle (Singleton управлява както своя екземпляр, така и бизнес логиката). Съвременното мобилно разработване предпочита DI (Dagger, Hilt, Swinject) за управление на единични екземпляри — DI контейнерът създава обекта веднъж и го инжектира чрез конструктор.
Swift Singleton се имплементира чрез статично свойство shared с частен инициализатор. От Swift 3 нататък, мързеливата инициализация на статични свойства е гарантирано нишково безопасна — компилаторът автоматично добавя синхронизация чрез dispatch_once. Достатъчно е да декларирате static let shared = Class() и да направите init() частен. Swift не изисква допълнителна синхронизация за еднонишков достъп след инициализация.
final class NetworkManager {
// Thread-safe Singleton
static let shared = NetworkManager()
private init() {
URLSessionConfiguration.default.timeoutIntervalForRequest = 30
}
private var cache = NSCache<NSString, NSData>()
func fetchData(from url: URL) async throws -> Data {
let key = url.absoluteString as NSString
if let cached = cache.object(forKey: key) {
return cached as Data
}
let (data, _) = try await URLSession.shared.data(from: url)
cache.setObject(data as NSData, forKey: key)
return data
}
}
// Използване
let data = try await NetworkManager.shared.fetchData(from: url)
Apple Singleton — в iOS SDK много обекти използват Singleton: UIApplication.shared, UIScreen.main, FileManager.default, NotificationCenter.default, UserDefaults.standard. Apple използва Singleton за услуги, които са физически уникални (един екран, едно приложение). Разработчиците копират този шаблон за своите услуги. В SwiftUI глобалният достъп до Singleton се заменя от Environment и @EnvironmentObject, което подобрява тестваемостта.
Kotlin Singleton — най-простият начин: ключовата дума object декларира клас-сингълтън с мързелива инициализация при първи достъп. Kotlin object е нишково безопасен и не изисква допълнителна синхронизация. Ако е необходим Singleton с параметри на конструктора, се използва companion object с lazy делегат. В Android Singleton често е необходим за контекста Application и услуги, инициализирани чрез Application.onCreate().
// Вариант 1: object — прост Singleton без параметри
object AppPreferences {
private val prefs = Application.instance
.getSharedPreferences("app", Context.MODE_PRIVATE)
var isFirstLaunch: Boolean
get() = prefs.getBoolean("first_launch", true)
set(value) = prefs.edit { putBoolean("first_launch", value) }
}
// Вариант 2: companion object — Singleton с параметри
class ApiClient private constructor(baseUrl: String) {
companion object {
@Volatile
private var instance: ApiClient? = null
fun getInstance(baseUrl: String): ApiClient {
return instance ?: this.synchronized {
instance ?: ApiClient(baseUrl).also { instance = it }
}
}
}
fun request(endpoint: String): String { /* ... */ }
}
Android SDK Singleton — много системни услуги на Android имплементират Singleton: context.getSystemService(), Room.databaseBuilder(), Retrofit.Builder(). Примерите включват SharedPreferences, MediaPlayer, AudioManager. В Android приложения Singleton често се използва за хранилища, мениджъри и фабрики. Google препоръчва замяна на Singleton с DI (Hilt, Koin), където Singleton обхватът (Scope.Singleton или @Singleton) се управлява от контейнера, а класът остава тестваем.
Thread safety — критично изискване за Singleton в многонишкова среда. Без синхронизация две нишки могат едновременно да проверят instance == null и да създадат два екземпляра. Решение — заключване при първото създаване и освобождаване след инициализация. В Swift статичните свойства (static let) са по подразбиране нишково безопасни. В Kotlin object е нишково безопасен. За Java стил в Kotlin се използва synchronized или @Volatile + double-check locking.
| Език | Механизъм | Нишкова безопасност | Мързелива инициализация |
|---|---|---|---|
| Swift | static let | dispatch_once (авто) | Да, при първи достъп |
| Kotlin object | Object declaration | Инициализаторът на класа е нишково безопасен | Да, при първи достъп |
| Kotlin companion | synchronized + @Volatile | Double-checked locking | Да, чрез lazy или synchronized |
| Java | synchronized + volatile | Double-checked locking | Да, в getInstance() |
Double-checked locking — шаблон за мързелива инициализация на Singleton. Първа проверка без синхронизация (бърза, ако екземплярът вече съществува), втора — вътре в synchronized (създаване само от една нишка). @Volatile гарантира видимост на промените за всички нишки. Без volatile друга нишка може да види частично създаден обект. В Kotlin lazy делегатът с LazyThreadSafetyMode.SYNCHRONIZED автоматично имплементира double-checked locking.
Dependency Injection — алтернатива на Singleton за управление на единичен екземпляр. DI контейнерът (Dagger, Hilt, Koin, Swinject) създава обекта веднъж в Singleton обхват и го инжектира чрез конструктор. Класът не знае за своя Singleton статус — това се решава от контейнера. Кодът става тестваем: в тест DI модулът се заменя с mock модул. Предимства на DI: изрични зависимости в конструктора, възможност за презаписване, единен жизнен цикъл.
Кога Singleton е оправдан — обекти на системно ниво: Crashlytics, Analytics, Logging. Тези услуги се инициализират веднъж в AppDelegate/Application и се използват навсякъде. DI е прекален за тях. Singleton е удобен и за кешове на изображения (NSCache, Coil, Glide), където глобалният достъп е оправдан от производителност. За всичко останало DI е за предпочитане: прави зависимостите видими, опростява тестването и рефакторирането.
Хибриден подход — Singleton с възможност за презаписване за тестове. В Swift се използва протокол + статично свойство, което тестът може да замени (напр. чрез URLProtocol за URLSession). В Kotlin — отворен клас с injectable свойство, където тестът задава mock чрез рефлексия или setter. Този подход запазва простотата на Singleton, но предоставя инструменти за тестване. Google препоръчва Hilt за Android, Apple не налага DI за iOS — изборът зависи от екипа.
Често задавани въпроси
Не, Singleton е GoF шаблон, но честата му неправилна употреба го превръща в анти-шаблон Global State. Singleton е оправдан за физически уникални ресурси (екран, принтер, файлова система). Проблеми възникват, когато Singleton се използва за управление на данни: скрити зависимости, трудност при тестване, нарушаване на Single Responsibility Principle. Съвременна алтернатива — DI с Singleton обхват.
Три подхода: (1) чрез протокол — Singleton имплементира протокол, тестовете заменят имплементацията; (2) чрез DI — Singleton се инжектира като зависимост чрез конструктор; (3) чрез reset метод — Singleton има метод за нулиране на състоянието в тестове (само за тест билд). Първият подход е за предпочитане, третият — опасен за production. Swift позволява замяна на shared свойството чрез runtime манипулации в тестове.
Kotlin object — езикова конструкция, която създава Singleton на ниво байткод. За разлика от Java имплементацията с частен конструктор и getInstance(), object гарантира нишкова безопасност, мързелива инициализация и забрана на наследяване. Java Singleton изисква ръчна синхронизация (synchronized) и volatile за коректна работа в многонишкова среда. Kotlin object — най-безопасният и сбит начин в Android.
Наследяването на Singleton нарушава шаблона: ако класът Singleton може да се наследява, подкласът може да създаде втори екземпляр, нарушавайки уникалността. В Swift final class забранява наследяване. Kotlin object не може да бъде наследен (object — sealed). Ако е необходим Singleton с вариативност, използвайте DI контейнер с Singleton обхват: гарантира един екземпляр и поддържа наследяване чрез интерфейси.
Параметрите се предават чрез init(context: Application) или getInstance(param). Kotlin object не приема параметри — използвайте companion object с фабричен метод getInstance(param). Hilt решава проблема: @Singleton + @Inject constructor(context: Application) — DI контейнерът инжектира Application контекста автоматично. За retrofit клиент параметрите (baseUrl, interceptors) се предават чрез builder в DI модула.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също