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 {
// 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 thread-safe и не требует дополнительной синхронизации. Если нужен 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-класс можно наследовать, то subclass может создать второй экземпляр, нарушив уникальность. В 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также