Singleton (enkele) — een creatiepatroon dat een enkele instantie van een klasse garandeert en een wereldwijd toegangspunt biedt. Singleton wordt veel gebruikt in mobiele ontwikkeling voor gedeelde bronnen: netwerkclients, databases, instellingenbeheerders. Het patroon is beschreven in het klassieke boek GoF (1994) en blijft een van de meest herkenbare. Meer — op Refactoring Guru: Singleton.
Belangrijkste
Singleton — een creatiepatroon beschreven door GoF (Gang of Four) in 1994. Het patroon lost twee taken op: het beperkt het maken van een klasse-instantie tot één object en biedt wereldwijde toegang tot dit object. Singleton is nuttig voor bronnen die uniek moeten zijn: een sessiefabriek, afbeeldingscache, databaseverbindingsbeheerder, Crashlytics of Analytics-client.
Implementatie van Singleton vereist een privé-constructor (voorkomt externe creatie), een statisch veld met een enkele instantie en een statische toegangsmethode (shared, instance, getInstance). Clients roepen Singleton.shared.method() aan zonder zich zorgen te maken over het maken van objecten. Het patroon is populair in iOS en Android: URLSession.shared, UserDefaults.standard, FirebaseApp.sharedInstance — dit zijn allemaal Singletons. Overmatig gebruik van Singleton leidt echter tot het anti-patroon Global State.
Problemen van Singleton — verborgen afhankelijkheden (klassen zijn impliciet afhankelijk van het Singleton-object), moeilijk testen (de instantie kan niet worden vervangen in een test zonder extra inspanning), schending van het Single Responsibility Principle (Singleton beheert zowel zijn eigen instantie als bedrijfslogica). Moderne mobiele ontwikkeling geeft de voorkeur aan DI (Dagger, Hilt, Swinject) voor het beheren van enkele instanties — de DI-container maakt het object één keer aan en injecteert het via de constructor.
Swift Singleton wordt geïmplementeerd via een statische shared-eigenschap met een privé-initialisator. Sinds Swift 3 is luie initialisatie van statische eigenschappen gegarandeerd thread-safe — de compiler voegt automatisch synchronisatie toe via dispatch_once. Het volstaat om static let shared = Class() te declareren en init() privé te maken. Swift vereist geen extra synchronisatie voor single-thread toegang na initialisatie.
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
}
}
// Gebruik
let data = try await NetworkManager.shared.fetchData(from: url)
Apple Singleton — in iOS SDK gebruiken veel objecten Singleton: UIApplication.shared, UIScreen.main, FileManager.default, NotificationCenter.default, UserDefaults.standard. Apple gebruikt Singleton voor diensten die fysiek uniek zijn (één scherm, één app). Ontwikkelaars kopiëren dit patroon voor hun eigen diensten. In SwiftUI wordt wereldwijde toegang tot Singleton vervangen door Environment en @EnvironmentObject, wat de testbaarheid verbetert.
Kotlin Singleton — de eenvoudigste manier: het sleutelwoord object declareert een singleton-klasse met luie initialisatie bij eerste toegang. Kotlin object is thread-safe en vereist geen extra synchronisatie. Als een Singleton met constructorparameters nodig is, wordt companion object met lazy-delegate gebruikt. In Android is Singleton vaak nodig voor de Application-context en services die worden geïnitialiseerd via Application.onCreate().
// Optie 1: object — eenvoudige Singleton zonder parameters
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) }
}
// Optie 2: companion object — Singleton met parameters
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 — veel systeemdiensten van Android implementeren Singleton: context.getSystemService(), Room.databaseBuilder(), Retrofit.Builder(). Voorbeelden zijn SharedPreferences, MediaPlayer, AudioManager. In Android-apps wordt Singleton vaak gebruikt voor repositories, beheerders en fabrieken. Google raadt aan om Singleton te vervangen door DI (Hilt, Koin), waarbij de Singleton-scope (Scope.Singleton of @Singleton) door de container wordt beheerd en de klasse testbaar blijft.
Thread safety — een kritieke vereiste voor Singleton in een multi-thread omgeving. Zonder synchronisatie kunnen twee threads tegelijk instance == null controleren en twee instanties maken. Oplossing — blokkering bij eerste creatie en vrijgave na initialisatie. In Swift zijn statische eigenschappen (static let) standaard thread-safe. In Kotlin is object thread-safe. Voor Java-stijl in Kotlin wordt synchronized of @Volatile + double-check locking gebruikt.
| Taal | Mechanisme | Thread-veiligheid | Luie initialisatie |
|---|---|---|---|
| Swift | static let | dispatch_once (auto) | Ja, bij eerste toegang |
| Kotlin object | Object declaration | Klasseninitialisator is thread-safe | Ja, bij eerste toegang |
| Kotlin companion | synchronized + @Volatile | Double-checked locking | Ja, via lazy of synchronized |
| Java | synchronized + volatile | Double-checked locking | Ja, in getInstance() |
Double-checked locking — patroon voor luie initialisatie van Singleton. Eerste controle zonder synchronisatie (snel, als de instantie al bestaat), tweede — binnen synchronized (creatie door slechts één thread). @Volatile garandeert zichtbaarheid van wijzigingen voor alle threads. Zonder volatile kan een andere thread een gedeeltelijk gemaakt object zien. In Kotlin implementeert de lazy-delegate met LazyThreadSafetyMode.SYNCHRONIZED automatisch double-checked locking.
Dependency Injection — een alternatief voor Singleton voor het beheren van een enkele instantie. De DI-container (Dagger, Hilt, Koin, Swinject) maakt het object één keer aan in Singleton-scope en injecteert het via de constructor. De klasse weet niet van zijn Singleton-status — dit wordt door de container bepaald. Code wordt testbaar: in de test wordt de DI-module vervangen door een mock-module. Voordelen van DI: expliciete afhankelijkheden in de constructor, overschrijfbaarheid, uniforme levenscyclus.
Wanneer Singleton gerechtvaardigd is — objecten op systeemniveau: Crashlytics, Analytics, Logging. Deze services worden eenmalig geïnitialiseerd in AppDelegate/Application en overal gebruikt. DI is voor hen overdreven. Singleton is ook handig voor afbeeldingscaches (NSCache, Coil, Glide), waar wereldwijde toegang gerechtvaardigd is door prestaties. Voor al het andere heeft DI de voorkeur: het maakt afhankelijkheden zichtbaar, vereenvoudigt testen en refactoring.
Hybride aanpak — Singleton met overschrijfbaarheid voor tests. In Swift wordt een protocol + statische eigenschap gebruikt die de test kan vervangen (bijv. via URLProtocol voor URLSession). In Kotlin — een open klasse met injectable-eigenschap, waar de test mock instelt via reflectie of setter. Deze aanpak behoudt de eenvoud van Singleton, maar biedt tools voor testen. Google beveelt Hilt aan voor Android, Apple dwingt DI niet af voor iOS — de keuze hangt af van het team.
Veelgestelde vragen
Nee, Singleton is een GoF-patroon, maar frequent verkeerd gebruik verandert het in het anti-patroon Global State. Singleton is gerechtvaardigd voor fysiek unieke bronnen (scherm, printer, bestandssysteem). Problemen ontstaan wanneer Singleton wordt gebruikt voor gegevensbeheer: verborgen afhankelijkheden, moeilijk testen, schending van het Single Responsibility Principle. Modern alternatief — DI met Singleton-scope.
Drie benaderingen: (1) via protocol — Singleton implementeert een protocol, tests vervangen de implementatie; (2) via DI — Singleton wordt als afhankelijkheid via de constructor geïnjecteerd; (3) via reset-methode — Singleton heeft een methode om de status in tests te resetten (alleen voor test-build). De eerste benadering heeft de voorkeur, de derde — is gevaarlijk voor productie. Swift maakt het mogelijk de shared-eigenschap te vervangen via runtime-manipulaties in tests.
Kotlin object — een taalconstructie die Singleton op bytecode-niveau creëert. In tegenstelling tot Java-implementatie met privé-constructor en getInstance(), garandeert object thread-veiligheid, luie initialisatie en verbod op overerving. Java Singleton vereist handmatige synchronisatie (synchronized) en volatile voor correcte werking in een multi-thread omgeving. Kotlin object — de veiligste en bondigste manier in Android.
Overerving van Singleton schendt het patroon: als de Singleton-klasse kan worden overgeërfd, kan een subklasse een tweede instantie maken, waardoor uniciteit wordt verbroken. In Swift verbiedt final class overerving. Kotlin object kan niet worden overgeërfd (object — sealed). Als een Singleton met variatie nodig is, gebruik dan een DI-container met Singleton-scope: deze garandeert één instantie en ondersteunt overerving via interfaces.
Parameters worden doorgegeven via init(context: Application) of getInstance(param). Kotlin object accepteert geen parameters — gebruik companion object met een fabrieksmethode getInstance(param). Hilt lost het probleem op: @Singleton + @Inject constructor(context: Application) — de DI-container injecteert de Application-context automatisch. Voor een retrofit-client worden parameters (baseUrl, interceptors) doorgegeven via een builder in de DI-module.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook