Singleton (одинак) — породжувальний патерн, що гарантує єдиний екземпляр класу та надає глобальну точку доступу до нього. Singleton широко застосовується в мобільній розробці для shared-ресурсів: мережеві клієнти, бази даних, менеджери налаштувань. Патерн описано в класичній книзі 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 {
// Потокобезпечний 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: явні залежності в конструкторі, можливість перевизначення, єдиний lifecycle.
Коли Singleton виправданий — об'єкти системного рівня: Crashlytics, Analytics, Logging. Ці сервіси ініціалізуються один раз в AppDelegate/Application і використовуються повсюдно. DI для них надлишковий. Singleton також зручний для кешів зображень (NSCache, Coil, Glide), де глобальний доступ виправданий продуктивністю. Для всього іншого кращий DI: він робить залежності видимими, спрощує тестування та рефакторинг.
Гібридний підхід — Singleton з можливістю перевизначення для тестів. У Swift використовується протокол + статична властивість, яку тест може замінити (наприклад, через URLProtocol для URLSession). В Kotlin — open class з 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) передаються через білдер в DI-модулі.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також