Singleton — model constructiv care garantează o singură instanță a clasei și oferă un punct global de acces la aceasta. Singleton este utilizat pe scară largă în dezvoltarea mobilă pentru resurse partajate: clienți de rețea, baze de date, manageri de setări. Modelul a fost descris în cartea clasică GoF (1994) și rămâne unul dintre cele mai cunoscute. Mai multe — pe Refactoring Guru: Singleton.
Principalul
Singleton — model constructiv descris de GoF (Gang of Four) în 1994. Modelul rezolvă două sarcini: limitează crearea instanței clasei la un singur obiect și oferă acces global la acest obiect. Singleton este util pentru resurse care trebuie să fie unice: fabrică de sesiuni, cache de imagini, manager de conexiune la baza de date, client Crashlytics sau Analytics.
Implementarea Singleton necesită un constructor privat (interzice crearea externă), un câmp static cu o singură instanță și o metodă statică de acces (shared, instance, getInstance). Clienții apelează Singleton.shared.method() fără a-și face griji despre crearea obiectului. Modelul este popular în iOS și Android: URLSession.shared, UserDefaults.standard, FirebaseApp.sharedInstance — toate sunt Singleton. Cu toate acestea, utilizarea excesivă a Singleton duce la anti-modelul Global State.
Problemele Singleton — dependențe ascunse (clasele depind implicit de obiectul Singleton), dificultatea testării (nu se poate înlocui instanța în test fără efort suplimentar), încălcarea Single Responsibility Principle (Singleton gestionează atât instanța sa, cât și logica de afaceri). Dezvoltarea mobilă modernă preferă DI (Dagger, Hilt, Swinject) pentru gestionarea instanțelor unice — containerul DI creează obiectul o dată și îl injectează prin constructor.
Swift Singleton este implementat prin proprietatea statică shared cu un inițializator privat. Începând cu Swift 3, inițializarea lenesă a proprietăților statice este garantată ca sigură pentru fire — compilatorul adaugă automat sincronizare prin dispatch_once. Este suficient să declarați static let shared = Class() și să faceți init() privat. Swift nu necesită sincronizare suplimentară pentru accesul cu un singur fir după inițializare.
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
}
}
// Utilizare
let data = try await NetworkManager.shared.fetchData(from: url)
Apple Singleton — în iOS SDK multe obiecte folosesc Singleton: UIApplication.shared, UIScreen.main, FileManager.default, NotificationCenter.default, UserDefaults.standard. Apple folosește Singleton pentru servicii care sunt fizic unice (un ecran, o aplicație). Dezvoltatorii copiază acest model pentru serviciile lor. În SwiftUI accesul global la Singleton este înlocuit de Environment și @EnvironmentObject, ceea ce îmbunătățește testabilitatea.
Kotlin Singleton — cel mai simplu mod: cuvântul cheie object declară o clasă-singleton cu inițializare lenesă la prima accesare. Kotlin object este sigur pentru fire și nu necesită sincronizare suplimentară. Dacă este necesar un Singleton cu parametri de constructor, se folosește companion object cu delegat lazy. În Android Singleton este adesea necesar pentru contextul Application și serviciile care sunt inițializate prin Application.onCreate().
// Varianta 1: object — Singleton simplu fără parametri
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) }
}
// Varianta 2: companion object — Singleton cu parametri
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 — multe servicii de sistem Android implementează Singleton: context.getSystemService(), Room.databaseBuilder(), Retrofit.Builder(). Exemplele includ SharedPreferences, MediaPlayer, AudioManager. În aplicațiile Android Singleton este adesea folosit pentru repository-uri, manageri și fabrici. Google recomandă înlocuirea Singleton cu DI (Hilt, Koin), unde domeniul Singleton (Scope.Singleton sau @Singleton) este gestionat de container, iar clasa rămâne testabilă.
Thread safety — cerință critică pentru Singleton în mediul multi-thread. Fără sincronizare, două fire pot verifica simultan instance == null și crea două instanțe. Soluția — blocare la prima creare și eliberare după inițializare. În Swift proprietățile statice (static let) sunt sigure pentru fire în mod implicit. În Kotlin object este sigur pentru fire. Pentru stilul Java în Kotlin se folosește synchronized sau @Volatile + double-check locking.
| Limbaj | Mecanism | Siguranța firelor | Inițializare lenesă |
|---|---|---|---|
| Swift | static let | dispatch_once (auto) | Da, la prima accesare |
| Kotlin object | Object declaration | Inițializatorul clasei este sigur pentru fire | Da, la prima accesare |
| Kotlin companion | synchronized + @Volatile | Double-checked locking | Da, prin lazy sau synchronized |
| Java | synchronized + volatile | Double-checked locking | Da, în getInstance() |
Double-checked locking — model pentru inițializarea lenesă a Singleton. Prima verificare fără sincronizare (rapidă, dacă instanța este deja creată), a doua — în interiorul synchronized (crearea doar de un singur fir). @Volatile garantează vizibilitatea modificărilor pentru toate firele. Fără volatile un alt fir poate vedea un obiect parțial creat. În Kotlin delegatul lazy cu LazyThreadSafetyMode.SYNCHRONIZED implementează automat double-checked locking.
Dependency Injection — alternativă la Singleton pentru gestionarea instanței unice. Containerul DI (Dagger, Hilt, Koin, Swinject) creează obiectul o dată în domeniul Singleton și îl injectează prin constructor. Clasa nu știe despre starea sa de Singleton — aceasta este decisă de container. Codul devine testabil: în test modulul DI este înlocuit cu un modul mock. Avantajele DI: dependențe explicite în constructor, posibilitatea de suprascriere, ciclu de viață unitar.
Când Singleton este justificat — obiecte la nivel de sistem: Crashlytics, Analytics, Logging. Aceste servicii sunt inițializate o dată în AppDelegate/Application și utilizate peste tot. DI este excesiv pentru ele. Singleton este, de asemenea, convenabil pentru cache-uri de imagini (NSCache, Coil, Glide), unde accesul global este justificat de performanță. Pentru orice altceva DI este preferabil: face dependențele vizibile, simplifică testarea și refactorizarea.
Abordarea hibridă — Singleton cu posibilitatea de suprascriere pentru teste. În Swift se folosește un protocol + o proprietate statică pe care testul o poate înlocui (de exemplu, prin URLProtocol pentru URLSession). În Kotlin — o clasă deschisă cu proprietate injectabilă, unde testul setează mock prin reflexie sau setter. Această abordare păstrează simplitatea Singleton, dar oferă instrumente pentru testare. Google recomandă Hilt pentru Android, Apple nu impune DI pentru iOS — alegerea depinde de echipă.
Întrebări frecvente
Nu, Singleton este un model GoF, dar utilizarea sa frecventă incorectă îl transformă în anti-modelul Global State. Singleton este justificat pentru resurse fizic unice (ecran, imprimantă, sistem de fișiere). Problemele apar când Singleton este folosit pentru gestionarea datelor: dependențe ascunse, dificultatea testării, încălcarea Single Responsibility Principle. Alternativa modernă — DI cu domeniul Singleton.
Trei abordări: (1) prin protocol — Singleton implementează protocolul, testele înlocuiesc implementarea; (2) prin DI — Singleton este injectat ca dependență prin constructor; (3) prin metoda reset — Singleton are o metodă pentru resetarea stării în teste (doar pentru build-ul de test). Prima abordare este preferată, a treia — periculoasă pentru producție. Swift permite înlocuirea proprietății shared prin manipulări runtime în teste.
Kotlin object — o construcție lingvistică care creează Singleton la nivel de bytecode. Spre deosebire de implementarea Java cu constructor privat și getInstance(), object garantează siguranța firelor, inițializarea lenesă și interzicerea moștenirii. Java Singleton necesită sincronizare manuală (synchronized) și volatile pentru funcționarea corectă în mediu multi-thread. Kotlin object — cel mai sigur și mai concis mod în Android.
Moștenirea Singleton încalcă modelul: dacă clasa Singleton poate fi moștenită, subclasa poate crea o a doua instanță, încălcând unicitatea. În Swift final class interzice moștenirea. Kotlin object nu poate fi moștenit (object — sealed). Dacă este necesar un Singleton cu variabilitate, folosiți containerul DI cu domeniul Singleton: garantează o singură instanță și suportă moștenirea prin interfețe.
Parametrii sunt transmiși prin init(context: Application) sau getInstance(param). Kotlin object nu acceptă parametri — folosiți companion object cu o metodă fabrică getInstance(param). Hilt rezolvă problema: @Singleton + @Inject constructor(context: Application) — containerul DI injectează contextul Application automat. Pentru clientul retrofit parametrii (baseUrl, interceptors) sunt transmiși prin builder în modulul DI.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și