Singleton — un pattern creazionale che garantisce un'unica istanza di classe e fornisce un punto di accesso globale ad essa. Singleton è ampiamente utilizzato nello sviluppo mobile per risorse condivise: client di rete, database, gestori di impostazioni. Il pattern è descritto nel libro classico GoF (1994) e rimane uno dei più riconoscibili. Maggiori informazioni su Refactoring Guru: Singleton.
Punti chiave
Singleton — un pattern di progettazione creazionale descritto da GoF (Gang of Four) nel 1994. Il pattern risolve due problemi: limita l'istanziazione di una classe a un singolo oggetto e fornisce accesso globale a tale oggetto. Singleton è utile per risorse che devono essere uniche: fabbriche di sessioni, cache di immagini, gestori di connessione al database, client Crashlytics o Analytics.
Implementazione di Singleton richiede un costruttore privato (impedisce la creazione esterna), un campo statico con l'unica istanza e un metodo di accesso statico (shared, instance, getInstance). I client chiamano Singleton.shared.method() senza preoccuparsi della creazione dell'oggetto. Il pattern è popolare in iOS e Android: URLSession.shared, UserDefaults.standard, FirebaseApp.sharedInstance — tutto questo è Singleton. Tuttavia, l'uso eccessivo di Singleton porta all'anti-pattern Global State.
Problemi di Singleton — dipendenze nascoste (le classi dipendono implicitamente dall'oggetto Singleton), complessità di test (non è possibile sostituire l'istanza nei test senza sforzo aggiuntivo), violazione del Principio di Responsabilità Unica (Singleton gestisce sia la propria istanza che la logica di business). Lo sviluppo mobile moderno preferisce DI (Dagger, Hilt, Swinject) per gestire istanze singole — il contenitore DI crea l'oggetto una volta e lo inietta attraverso il costruttore.
Swift Singleton è implementato tramite una proprietà statica shared con un inizializzatore privato. Da Swift 3, l'inizializzazione pigra delle proprietà statiche è garantita thread-safe — il compilatore aggiunge automaticamente sincronizzazione tramite dispatch_once. Basta dichiarare static let shared = Class() e rendere init() privato. Swift non richiede sincronizzazione aggiuntiva per l'accesso a thread singolo dopo l'inizializzazione.
final class NetworkManager {
// Singleton thread-safe
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
}
}
// Utilizzo
let data = try await NetworkManager.shared.fetchData(from: url)
Apple Singleton — molti oggetti dell'SDK iOS usano Singleton: UIApplication.shared, UIScreen.main, FileManager.default, NotificationCenter.default, UserDefaults.standard. Apple usa Singleton per servizi fisicamente unici (uno schermo, un'applicazione). Gli sviluppatori copiano questo pattern per i propri servizi. In SwiftUI, l'accesso globale a Singleton viene sostituito da Environment e @EnvironmentObject, migliorando la testabilità.
Kotlin Singleton — il modo più semplice: la parola chiave object dichiara una classe singleton con inizializzazione pigra al primo accesso. Kotlin object è thread-safe e non richiede sincronizzazione aggiuntiva. Se è necessario un Singleton con parametri del costruttore, si usa un companion object con un delegato lazy. In Android, Singleton è spesso necessario per il contesto Application e i servizi che si inizializzano tramite Application.onCreate().
// Opzione 1: object — Singleton semplice senza 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) }
}
// Opzione 2: companion object — Singleton con 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 { /* ... */ }
}
Singleton dell'SDK Android — molti servizi di sistema Android implementano Singleton: context.getSystemService(), Room.databaseBuilder(), Retrofit.Builder(). Gli esempi includono SharedPreferences, MediaPlayer, AudioManager. Nelle applicazioni Android, Singleton è spesso usato per repository, gestori e fabbriche. Google consiglia di sostituire Singleton con DI (Hilt, Koin), dove l'ambito Singleton (Scope.Singleton o @Singleton) è gestito dal contenitore mentre la classe rimane testabile.
Thread safety — un requisito critico per Singleton in ambienti multithread. Senza sincronizzazione, due thread possono controllare simultaneamente instance == null e creare due istanze. La soluzione è bloccare durante la prima creazione e rilasciare dopo l'inizializzazione. In Swift, le proprietà statiche (static let) sono thread-safe per impostazione predefinita. In Kotlin, object è thread-safe. Per lo stile Java in Kotlin, si usa synchronized o @Volatile + double-check locking.
| Linguaggio | Meccanismo | Sicurezza dei thread | Inizializzazione pigra |
|---|---|---|---|
| Swift | static let | dispatch_once (automatico) | Sì, al primo accesso |
| Kotlin object | Dichiarazione di object | Inizializzatore di classe thread-safe | Sì, al primo accesso |
| Kotlin companion | synchronized + @Volatile | Double-checked locking | Sì, tramite lazy o synchronized |
| Java | synchronized + volatile | Double-checked locking | Sì, in getInstance() |
Double-checked locking — un pattern per l'inizializzazione pigra di Singleton. Primo controllo senza sincronizzazione (veloce se l'istanza esiste già), secondo all'interno di synchronized (creazione da un solo thread). @Volatile garantisce la visibilità delle modifiche per tutti i thread. Senza volatile, un altro thread potrebbe vedere un oggetto parzialmente costruito. In Kotlin, il delegato lazy con LazyThreadSafetyMode.SYNCHRONIZED implementa automaticamente double-checked locking.
Dependency Injection — un'alternativa a Singleton per gestire istanze singole. Un contenitore DI (Dagger, Hilt, Koin, Swinject) crea l'oggetto una volta in un ambito Singleton e lo inietta attraverso il costruttore. La classe non conosce il suo stato Singleton — il contenitore decide. Il codice diventa testabile: il modulo DI viene sostituito con un modulo mock nei test. Vantaggi di DI: dipendenze esplicite nel costruttore, possibilità di override, ciclo di vita unificato.
Quando Singleton è giustificato — oggetti a livello di sistema: Crashlytics, Analytics, Logging. Questi servizi vengono inizializzati una volta in AppDelegate/Application e usati ovunque. DI è eccessivo per loro. Singleton è anche conveniente per cache di immagini (NSCache, Coil, Glide), dove l'accesso globale è giustificato dalle prestazioni. Per tutto il resto, DI è preferibile: rende le dipendenze visibili, semplifica i test e il refactoring.
Approccio ibrido — Singleton con possibilità di override per i test. In Swift, un protocollo + proprietà statica che i test possono sostituire (ad esempio, tramite URLProtocol per URLSession). In Kotlin, una classe aperta con una proprietà injectable, dove i test impostano un mock tramite riflessione o un setter. Questo approccio mantiene la semplicità di Singleton ma fornisce capacità di test. Google consiglia Hilt per Android, Apple non impone DI per iOS — la scelta dipende dal team.
Domande frequenti
No, Singleton è un pattern GoF, ma il suo uso scorretto frequente lo trasforma nell'anti-pattern Global State. Singleton è giustificato per risorse fisicamente uniche (schermo, stampante, file system). I problemi sorgono quando Singleton viene utilizzato per la gestione dei dati: dipendenze nascoste, complessità di test, violazione del Principio di Responsabilità Unica. Un'alternativa moderna è DI con ambito Singleton.
Tre approcci: (1) tramite protocollo — Singleton implementa un protocollo, i test scambiano l'implementazione; (2) tramite DI — Singleton viene iniettato come dipendenza attraverso il costruttore; (3) tramite metodo reset — Singleton ha un metodo per resettare lo stato nei test (solo per build di test). Il primo approccio è preferibile, il terzo è pericoloso per la produzione. Swift consente di sostituire la proprietà shared tramite manipolazione runtime nei test.
Kotlin object è una costruzione linguistica che crea un Singleton a livello di bytecode. A differenza dell'implementazione Java con costruttore privato e getInstance(), object garantisce thread safety, inizializzazione pigra e proibisce l'ereditarietà. Java Singleton richiede sincronizzazione manuale (synchronized) e volatile per funzionare correttamente in ambienti multithread. Kotlin object è il modo più sicuro e conciso in Android.
Ereditare Singleton rompe il pattern: se una classe Singleton può essere ereditata, una sottoclasse potrebbe creare una seconda istanza, violando l'unicità. In Swift, final class proibisce l'ereditarietà. Kotlin object non può essere ereditato (object è sealed). Se è necessario un Singleton con variabilità, usa un contenitore DI con ambito Singleton: garantisce un'istanza singola e supporta l'ereditarietà tramite interfacce.
I parametri vengono passati tramite init(context: Application) o getInstance(param). Kotlin object non accetta parametri — usa un companion object con un metodo factory getInstance(param). Hilt risolve il problema: @Singleton + @Inject constructor(context: Application) — il contenitore DI inietta automaticamente il contesto Application. Per un client Retrofit, i parametri (baseUrl, interceptors) vengono passati tramite un builder nel modulo DI.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche