Singleton — що це таке, одиничний екземпляр класу в iOS та Android

Автор: IT Sectr Опубліковано: 2026-02-17 Час читання: 8 хв

Singleton (одинак) — породжувальний патерн, що гарантує єдиний екземпляр класу та надає глобальну точку доступу до нього. Singleton широко застосовується в мобільній розробці для shared-ресурсів: мережеві клієнти, бази даних, менеджери налаштувань. Патерн описано в класичній книзі GoF (1994) і він залишається одним із найвідоміших. Детальніше — на Refactoring Guru: Singleton.

Головне

  • Singleton — гарантує один екземпляр класу на весь додаток
  • Глобальна точка доступу — статична властивість shared або companion object
  • Thread safety — потрібна синхронізація для коректної роботи в багатопотоковому середовищі
  • Критика — Singleton ускладнює тестування та створює приховані залежності
  • Альтернативи — Dependency Injection, Service Locator для заміни Singleton

Що таке 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 контейнер створює об'єкт один раз і впроваджує його через конструктор.

Singleton в iOS на Swift: shared та статичні властивості

Swift Singleton реалізується через статичну властивість shared з приватним ініціалізатором. Починаючи з Swift 3, лінива ініціалізація статичних властивостей гарантовано потокобезпечна — компілятор автоматично додає синхронізацію через dispatch_once. Достатньо оголосити static let shared = Class() і зробити init() приватним. Swift не вимагає додаткової синхронізації для однопотокового доступу після ініціалізації.

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, що покращує тестованість.

Singleton в Android на Kotlin: companion object та object

Kotlin Singleton — найпростіший спосіб: ключове слово object оголошує клас-синглтон з лінивою ініціалізацією при першому зверненні. Kotlin object потокобезпечний і не потребує додаткової синхронізації. Якщо потрібен Singleton з параметрами конструктора, використовується companion object з lazy-делегатом. В Android Singleton часто необхідний для Application-контексту та сервісів, які ініціалізуються через Application.onCreate().

kotlin
// Варіант 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: dispatch_once, synchronized та lock

Thread safety — критична вимога для Singleton у багатопотоковому середовищі. Без синхронізації два потоки можуть одночасно перевірити instance == null і створити два екземпляри. Рішення — блокування при першому створенні та звільнення після ініціалізації. В Swift статичні властивості (static let) потокобезпечні за замовчуванням. В Kotlin object потокобезпечний. Для Java-стилю в Kotlin використовується synchronized або @Volatile + double-check locking.

МоваМеханізмПотокобезпекаЛінива ініціалізація
Swiftstatic letdispatch_once (авто)Так, при першому зверненні
Kotlin objectObject declarationІніціалізатор класу потокобезпечнийТак, при першому зверненні
Kotlin companionsynchronized + @VolatileDouble-checked lockingТак, через lazy або synchronized
Javasynchronized + volatileDouble-checked lockingТак, у getInstance()

Double-checked locking — патерн для лінивої ініціалізації Singleton. Перша перевірка без синхронізації (швидка, якщо екземпляр уже створено), друга — всередині synchronized (створення лише одним потоком). @Volatile гарантує видимість змін для всіх потоків. Без volatile інший потік може побачити частково створений об'єкт. В Kotlin lazy-делегат з LazyThreadSafetyMode.SYNCHRONIZED автоматично реалізує double-checked locking.

Singleton vs Dependency Injection: коли використовувати

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 — це антипатерн?

Ні, Singleton — патерн GoF, але його часте неправильне використання перетворює його на антипатерн Global State. Singleton виправданий для фізично унікальних ресурсів (екран, принтер, файлова система). Проблеми виникають, коли Singleton використовується для керування даними: приховані залежності, складність тестування, порушення Single Responsibility Principle. Сучасна альтернатива — DI з Singleton-скоупом.

Як тестувати код, що використовує Singleton?

Три підходи: (1) через протокол — Singleton реалізує протокол, тести підміняють реалізацію; (2) через DI — Singleton впроваджується як залежність через конструктор; (3) через reset-метод — Singleton має метод для скидання стану в тестах (тільки для тестового білда). Перший підхід кращий, третій — небезпечний для production. Swift дозволяє замінити властивість shared через runtime-маніпуляції в тестах.

Чим Kotlin object відрізняється від Java Singleton?

Kotlin object — мовна конструкція, яка створює Singleton на рівні байткоду. На відміну від Java-реалізації з приватним конструктором та getInstance(), object гарантує потокобезпеку, ліниву ініціалізацію та заборону успадкування. Java Singleton вимагає ручної синхронізації (synchronized) та volatile для коректної роботи в багатопотоковому середовищі. Kotlin object — найбезпечніший та найлаконічніший спосіб в Android.

Чи можна успадковувати Singleton?

Успадкування Singleton порушує патерн: якщо Singleton-клас можна успадкувати, то підклас може створити другий екземпляр, порушивши унікальність. В Swift final class забороняє успадкування. Kotlin object не може бути успадкований (object — sealed). Якщо потрібен Singleton з варіативністю, використовуйте DI-контейнер з Singleton-скоупом: він гарантує один екземпляр і підтримує успадкування через інтерфейси.

Як передати параметри в Singleton в Android?

Параметри передаються через init(context: Application) або getInstance(param). Kotlin object не приймає параметри — використовуйте companion object з фабричним методом getInstance(param). Hilt вирішує проблему: @Singleton + @Inject constructor(context: Application) — DI-контейнер впроваджує Application-контекст автоматично. Для retrofit-клієнта параметри (baseUrl, interceptors) передаються через білдер в DI-модулі.

Підсумки

  • Singleton — патерн з єдиним екземпляром і глобальним доступом
  • Swift shared — static let з потокобезпекою від компілятора
  • Kotlin object — лінива ініціалізація без додаткового коду
  • Thread safety — double-checked locking для Java, автоматична для Swift/Kotlin
  • Apple SDK — UIApplication.shared, UserDefaults.standard, FileManager.default
  • Android SDK — Retrofit, Room, SharedPreferences через Singleton-менеджери
  • Альтернативи — Dependency Injection для тестованого коду

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також