Service Locator — essentie van het patroon, centraal register en DI

Auteur: IT Sectr Gepubliceerd: 2026-02-18 Leestijd: 9 min

Service Locator — een architectuurpatroon dat een centraal register (registry) van diensten biedt. Clientcode vraagt een dienst aan via een statische locator, zonder deze direct aan te maken of via een constructor te ontvangen. Service Locator wordt vaak beschouwd als een alternatief voor Dependency Injection: het is eenvoudiger te implementeren, maar verbergt afhankelijkheden en bemoeilijkt testen. Het patroon wordt geïmplementeerd via een globale Singleton-klasse met registratie en resolutie van diensten. Meer — in de vergelijking van DI en Service Locator door Martin Fowler.

Belangrijkste

  • Service Locator — centraal register (registry) voor het verkrijgen van diensten
  • Alternatief voor DI — eenvoudiger te implementeren, maar verbergt afhankelijkheden voor de client
  • Antipatroon — veel ontwikkelaars beschouwen Service Locator als een antipatroon vanwege verborgen afhankelijkheden
  • Globale toegang — de locator is statisch beschikbaar vanaf elke locatie, zonder overdracht via constructor
  • Testen — moeilijker dan DI: vereist configuratie van de globale locator voor elke test

Wat is Service Locator: essentie en structuur van het patroon

Service Locator — een patroon dat het aanmaken en leveren van diensten centraliseert. Aan de basis staat een Singleton-klasse (Locator) die een register (Registry) van diensten bevat: een woordenboek waar de sleutel het type (of identificatie) van de dienst is en de waarde de concrete implementatie. Clientcode roept ServiceLocator.resolve(ServiceProtocol.self) aan en ontvangt een kant-en-klare instantie. Het patroon schrijft niet voor hoe de dienst wordt aangemaakt — fabriek, DI-container of new binnen de locator.

Structuur van het patroon — Registry (register van type [String: Any]), Locator (statische klasse met register en resolve), Service (registreerbare dienst). Het register kan fabrieken (closure/lambda voor het maken van objecten) of kant-en-klare instanties opslaan. De locator kan globaal zijn (één per applicatie) of met scope (per feature/module). Resolutie van een dienst is een opzoeking in het woordenboek op type. Swift en Kotlin gebruiken het type als sleutel via metatypen: ObjectIdentifier(ServiceProtocol.self).

ComponentVerantwoordelijkheidSwift/Kotlin
ServiceLocatorGlobale toegang tot dienstenclass ServiceLocator
RegistryOpslag van fabrieken/instanties[ObjectIdentifier: Any]
ServiceConcrete implementatieNetworkService()

Geschiedenis van het patroon — Service Locator werd beschreven in het boek Java Patterns (1998) en later in Core J2EE Patterns (2001). Martin Fowler vergelijkt in een artikel uit 2004 Service Locator met DI, waarbij hij opmerkt dat Service Locator „een eenvoudiger alternatief is, maar slechter voor testen“. In mobiele ontwikkeling werd Service Locator gebruikt in vroege Android-projecten en iOS-applicaties vóór de komst van Dagger en Swinject. Nu komt het patroon vaker voor in legacy-projecten en prototypen.

Service Locator in Swift: globaal dienstenregister

Service Locator in Swift — implementatie via statische eigenschappen en een thread-safe woordenboek. Er wordt een register gebruikt met ObjectIdentifier(Protocol.self) als sleutel en fabrieken (()->Any) als waarden. Lazy initialisatie (lazy var) — standaardpraktijk: de dienst wordt aangemaakt bij het eerste verzoek. Swift vereist expliciete typeconversie bij resolve: guard let service = locator.resolve(ServiceProtocol.self) else { return }.

swift
final class ServiceLocator {
    static let shared = ServiceLocator()
    private var registry: [ObjectIdentifier: Any] = [:]
    private let lock = NSLock()

    func register<T>(_ type: T.Type, factory: @escaping () -> T) {
        lock.lock()
        registry[ObjectIdentifier(type)] = factory
        lock.unlock()
    }

    func resolve<T>(_ type: T.Type) -> T {
        lock.lock()
        defer { lock.unlock() }
        guard let factory = registry[ObjectIdentifier(type)] as? () -> T else {
            fatalError("Service \(type) not registered")
        }
        return factory()
    }

    func reset() {
        lock.lock()
        registry.removeAll()
        lock.unlock()
    }
}

// Registratie
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }

// Gebruik
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)

Scopebeheer — de locator kan fabrieken (transient — telkens een nieuw object) of kant-en-klare instanties (singleton) opslaan. Voor fabrieken wordt een closure geregistreerd die bij elke resolve wordt aangeroepen. Voor singleton — een closure met capture van de aangemaakte instantie. Scope toevoegen: .transient, .singleton, .weak (zwakke referentie — het object leeft zolang iemand een referentie vasthoudt). Weak scope is handig voor UIKit ViewController om geheugenlekken bij pop/dismiss te voorkomen.

Service Locator in Kotlin: luie registratie

Service Locator in Kotlin — een compacte implementatie via object (Singleton) met inline reified functies voor typeveiligheid. Kotlin maakt het mogelijk een beknopte locator te maken: val service by locator<ServiceProtocol>() met een delegate, wat de code schoner maakt. Reified generics (<reified T>) vervangen ObjectIdentifier — het type wordt uit de generic verkregen. Kotlin-locators gebruiken vaak ConcurrentHashMap voor thread-veiligheid zonder expliciete vergrendelingen.

kotlin
object ServiceLocator {
    private val registry = ConcurrentHashMap<Class<*>, () -> Any>()

    inline fun <reified T: Any> register(noinline factory: () -> T) {
        registry[T::class.java] = factory
    }

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T: Any> resolve(): T {
        val factory = registry[T::class.java]
            ?: throw IllegalStateException("Service ${T::class.simpleName} not registered")
        return factory() as T
    }

    fun clear() {
        registry.clear()
    }
}

// Registratie
ServiceLocator.register<ApiService> { RetrofitApiService() }

// Gebruik in klasse
class UserRepository {
    private val api: ApiService = ServiceLocator.resolve()
    private val db: Database = ServiceLocator.resolve()
}

// Delegate voor luie resolutie
class LocatorDelegate<reified T: Any> : Lazy<T> {
    override val value: T get() = ServiceLocator.<T>resolve()
    override fun isInitialized(): Boolean = true
}

inline fun <reified T: Any> locator(): Lazy<T> = LocatorDelegate()
// Gebruik: val api by locator()

Service Locator in Android — komt voor in oude projecten vóór de komst van Dagger. Jetpack Hilt en Koin hebben Service Locator in de Android-gemeenschap verdrongen. De locator blijft echter relevant voor unittesten: een eenvoudige imitatie van ServiceLocator met mock-diensten zonder Hilt. Plus: niet wachten op compilatie van Dagger voor tests. Minus: als u vergeet de locator in de test te overschrijven, gebruiken tests productiediensten.

Service Locator vs DI: vergelijking en wanneer wat te kiezen

Zichtbaarheid van afhankelijkheden — het belangrijkste verschil. DI declareert afhankelijkheden expliciet: init(service: ServiceProtocol) — elke IDE toont de afhankelijkheden van de klasse. Service Locator verbergt ze: dependencies = ServiceLocator.resolve() — verborgen in de methode. Met DI kunt u alle afhankelijkheden van de klasse direct zien, met Service Locator moet u de hele klasse lezen. Dit maakt code met Service Locator minder voorspelbaar: een wijziging in het register kan elke klasse breken die de locator gebruikt.

KenmerkService LocatorDependency Injection
Zichtbaarheid van afhankelijkhedenVerborgen in methode-lichamenExpliciet in constructor
TestenConfiguratie van globaal registerMock in constructor
ModulariteitGlobaal register — niet modulairModules met verschillende containers
ComplexiteitEenvoudige implementatie, 50-100 regelsVereist Dagger/Swinject
Time-to-marketSnelle startConfiguratie van container

Wanneer Service Locator gerechtvaardigd is — prototypen en MVP (snelle start zonder configuratie). Legacy-projecten waar geen DI-framework kan worden toegevoegd (complexe compilatie, linter-beperkingen). Instrumentele bibliotheken (logging, crash-reporting) — ze zijn toch al globaal. Voor productieapplicaties met een team van 3 ontwikkelaars wordt DI aanbevolen: expliciete afhankelijkheden verminderen het aantal fouten bij refactoring en vergemakkelijken de introductie van nieuwe ontwikkelaars in het project.

Problemen van Service Locator en alternatieven

Verborgen afhankelijkheden — een klasse die ServiceLocator.resolve() binnen een methode gebruikt, kan niet statisch worden geanalyseerd. De IDE toont de afhankelijkheden niet, de compiler controleert niet of de dienst is geregistreerd. De fout „Service not registered“ treedt alleen op tijdens runtime. Refactoring wordt riskant: het verwijderen van een dienst uit het register kan elke klasse in de applicatie breken. DI lost dit probleem op door compilatiecontroles (Dagger) of expliciete constructors.

Testprobleem — elke test moet ServiceLocator.shared configureren met alle afhankelijkheden. Na de test — de status resetten (reset()). Bij parallelle uitvoering van tests leidt de globale status van ServiceLocator.shared tot race conditions: de ene test registreert een mock, een andere — ontvangt een mock van iemand anders. Oplossing: locators met scope (één per test) of ThreadLocal. DI lost dit probleem bij de wortel op: elke test maakt zijn eigen instantie met mock-afhankelijkheden.

swift
// Probleem van het testen van Service Locator
class LoginViewModelTests: XCTestCase {
    override func setUp() {
        super.setUp()
        // Configuratie van globaal register voor test
        ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
    }

    override func tearDown() {
        ServiceLocator.shared.reset()
        super.tearDown()
    }

    func testLogin() {
        let viewModel = LoginViewModel() // gebruikt ServiceLocator binnenin
        // test...
    }
}

Alternatieven voor Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — een eenvoudig patroon zonder globale status: een fabrieksklasse maakt diensten aan en wordt via de constructor doorgegeven. Ambient Context — een thread-safe alternatief voor cross-cutting concerns (logging, autorisatie). Het beste alternatief — Constructor Injection met handmatige fabriek zonder DI-framework: expliciete aanmaak van afhankelijkheden in de fabriek met overdracht via constructor geeft de duidelijkheid van DI zonder de complexiteit van Dagger/Swinject-configuratie.

Veelgestelde vragen

Is Service Locator een antipatroon?

Veel ontwikkelaars beschouwen Service Locator als een antipatroon omdat het afhankelijkheden verbergt, testen bemoeilijkt en verborgen koppelingen tussen klassen creëert. In prototypen, kleine projecten en voor globale diensten (logging, analyses) kan Service Locator echter gerechtvaardigd zijn. De beslissing hangt af van de context: voor een productieapplicatie met een team — DI, voor een enkele ontwikkelaar aan een prototype — Service Locator.

Waarin verschilt Service Locator van een DI-container?

Een DI-container (Dagger, Swinject) injecteert afhankelijkheden automatisch in een object — het object weet niet van het bestaan van de container. Service Locator — het object vraagt zelf afhankelijkheden op uit het register. Een DI-container volgt het IoC-principe, Service Locator — schendt het: het object beheert zelf het verkrijgen van zijn afhankelijkheden. Een DI-container werkt vóór het aanmaken van het object (via constructor), Service Locator — op elke plek in de code.

Wanneer Service Locator in iOS gebruiken?

Service Locator is gerechtvaardigd in iOS voor: globale diensten (Analytics, Logger, Crashlytics), prototypen waar configuratie van Swinject overdreven is, en voor unittesten van grote legacy-modules. Voor nieuwe iOS-projecten wordt Swinject of handmatige DI via constructor aanbevolen. SwiftUI met Environment — ook een vorm van DI die Service Locator vermijdt.

Hoe race conditions in Service Locator voorkomen?

Gebruik een thread-safe collectie (NSLock in Swift, ConcurrentHashMap in Kotlin). Voor tests — ThreadLocal of een locator met scope. Alternatief: async-local storage — diensten gebonden aan een coroutine/actor. De beste oplossing — vermijd Service Locator voor parallelle tests en gebruik DI met expliciete objectcreatie voor elke test.

Is Service Locator een singleton?

Meestal wordt Service Locator geïmplementeerd als Singleton, maar dit is niet verplicht. U kunt een locator-instantie voor een module (feature-scoped locator) maken en deze via de constructor doorgeven. Een feature-scoped locator lost het probleem van globale status op, maar lost het probleem van verborgen afhankelijkheden niet op. Zo'n patroon wordt Ambient Context of Scoped Locator genoemd.

Samenvatting

  • Service Locator — centraal register voor het verkrijgen van diensten via statische toegang
  • Verborgen afhankelijkheden — het grootste nadeel: afhankelijkheden zijn niet zichtbaar in de handtekening van de klasse
  • Testen — moeilijker dan DI vanwege globale status en noodzaak tot resetten
  • Swift/Kotlin — implementatie via thread-safe woordenboek en reified generics
  • Alternatief — DI (Dagger Hilt, Swinject) — standaard voor productieprojecten
  • Gerechtvaardigd — in prototypen, voor globale diensten en legacy-projecten

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.

Bespreek het project

Lees ook