Service Locator — mönstrets kärna, centralt register och DI

Författare: IT Sectr Publicerad: 2026-02-18 Lästid: 9 min

Service Locator — ett arkitekturmönster som tillhandahåller ett centralt register (registry) över tjänster. Klientkod begär en tjänst via en statisk lokator, utan att skapa den direkt eller ta emot den via konstruktor. Service Locator betraktas ofta som ett alternativ till Dependency Injection: det är enklare att implementera men döljer beroenden och försvårar testning. Mönstret implementeras via en global Singleton-klass med registrering och upplösning av tjänster. Mer — i jämförelsen av DI och Service Locator av Martin Fowler.

Huvudpunkter

  • Service Locator — centralt register (registry) för att hämta tjänster
  • Alternativ till DI — enklare att implementera, men döljer beroenden för klienten
  • Antimönster — många utvecklare anser Service Locator vara ett antimönster på grund av dolda beroenden
  • Global åtkomst — lokatorn är statiskt tillgänglig från var som helst, utan överföring via konstruktor
  • Testning — svårare än DI: kräver konfiguration av den globala lokatorn för varje test

Vad är Service Locator: mönstrets kärna och struktur

Service Locator — ett mönster som centraliserar skapande och tillhandahållande av tjänster. I grunden finns en Singleton-klass (Locator) som innehåller ett register (Registry) över tjänster: en ordbok där nyckeln är typen (eller identifieraren) av tjänsten och värdet är den konkreta implementeringen. Klientkod anropar ServiceLocator.resolve(ServiceProtocol.self) och får en färdig instans. Mönstret föreskriver inte hur tjänsten skapas — fabrik, DI-container eller new inuti lokatorn.

Mönstrets struktur — Registry (register av typ [String: Any]), Locator (statisk klass med register och resolve), Service (registrerbar tjänst). Registret kan lagra fabriker (closure/lambda för att skapa objekt) eller färdiga instanser. Lokatorn kan vara global (en per applikation) eller med omfattning (per feature/modul). Upplösning av tjänst är en uppslagning i ordboken efter typ. Swift och Kotlin använder typen som nyckel via metatyper: ObjectIdentifier(ServiceProtocol.self).

KomponentAnsvarSwift/Kotlin
ServiceLocatorGlobal åtkomst till tjänsterclass ServiceLocator
RegistryFörvaring av fabriker/instanser[ObjectIdentifier: Any]
ServiceKonkret implementeringNetworkService()

Mönstrets historia — Service Locator beskrevs i boken Java Patterns (1998) och senare i Core J2EE Patterns (2001). Martin Fowler jämför i en artikel från 2004 Service Locator med DI och noterar att Service Locator är „ett enklare alternativ, men sämre för testning“. Inom mobilutveckling användes Service Locator i tidiga Android-projekt och iOS-applikationer före tillkomsten av Dagger och Swinject. Nu förekommer mönstret oftare i legacy-projekt och prototyper.

Service Locator i Swift: globalt tjänsteregister

Service Locator i Swift — implementering via statiska egenskaper och en trådsäker ordbok. Ett register används med ObjectIdentifier(Protocol.self) som nyckel och fabriker (()->Any) som värden. Lat initialisering (lazy var) — standardpraxis: tjänsten skapas vid första begäran. Swift kräver explicit typkonvertering vid 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()
    }
}

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

// Användning
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)

Omfattningshantering — lokatorn kan lagra fabriker (transient — nytt objekt varje gång) eller färdiga instanser (singleton). För fabriker registreras en closure som anropas vid varje resolve. För singleton — closure med infångning av den skapade instansen. Lägga till omfattning: .transient, .singleton, .weak (svag referens — objektet lever så länge någon håller en referens). Weak-omfattning är praktisk för UIKit ViewController för att undvika minnesläckor vid pop/dismiss.

Service Locator i Kotlin: lat registrering

Service Locator i Kotlin — en kompakt implementering via object (Singleton) med inline reified-funktioner för typsäkerhet. Kotlin tillåter skapande av en kortfattad lokator: val service by locator<ServiceProtocol>() med delegat, vilket gör koden renare. Reified generics (<reified T>) ersätter ObjectIdentifier — typen hämtas från generiken. Kotlin-lokatorer använder ofta ConcurrentHashMap för trådsäkerhet utan explicita låsningar.

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()
    }
}

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

// Användning i klass
class UserRepository {
    private val api: ApiService = ServiceLocator.resolve()
    private val db: Database = ServiceLocator.resolve()
}

// Delegat för lat upplösning
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()
// Användning: val api by locator()

Service Locator i Android — förekommer i gamla projekt före tillkomsten av Dagger. Jetpack Hilt och Koin har ersatt Service Locator i Android-gemenskapen. Lokatorn förblir dock relevant för enhetstester: en enkel imitation av ServiceLocator med mock-tjänster utan Hilt. Plus: ingen väntan på Dagger-kompilering för tester. Minus: om du glömmer att åsidosätta lokatorn i testet använder testerna produktionstjänster.

Service Locator vs DI: jämförelse och när man väljer vad

Synlighet av beroenden — den största skillnaden. DI deklarerar beroenden explicit: init(service: ServiceProtocol) — alla IDE visar klassens beroenden. Service Locator döljer dem: dependencies = ServiceLocator.resolve() — dolt inuti metoden. Med DI kan du omedelbart se alla klassens beroenden, med Service Locator måste du läsa hela klasskroppen. Detta gör kod med Service Locator mindre förutsägbar: en ändring i registret kan bryta vilken klass som helst som använder lokatorn.

EgenskapService LocatorDependency Injection
Synlighet av beroendenDolda i metodkropparExplicita i konstruktor
TestningKonfiguration av globalt registerMock i konstruktor
ModularitetGlobalt register — inte modulärtModuler med olika containrar
KomplexitetEnkel implementering, 50-100 raderKräver Dagger/Swinject
Time-to-marketSnabb startKonfiguration av container

När Service Locator är befogat — prototyper och MVP (snabb start utan konfiguration). Legacy-projekt där DI-ramverk inte kan läggas till (komplex kompilering, linter-begränsningar). Instrumentella bibliotek (loggning, krashrapportering) — de är ändå globala. För produktionsapplikationer med ett team på 3 utvecklare rekommenderas DI: explicita beroenden minskar antalet fel vid omfaktorisering och förenklar introduktionen av nya utvecklare i projektet.

Problem med Service Locator och alternativ

Dolda beroenden — en klass som använder ServiceLocator.resolve() inuti en metod kan inte analyseras statiskt. IDE visar inte beroenden, kompilatorn kontrollerar inte om tjänsten är registrerad. Felet „Service not registered“ uppstår endast vid körning. Omfaktorisering blir riskabel: borttagning av en tjänst från registret kan bryta vilken klass som helst i applikationen. DI löser detta problem genom kompileringskontroller (Dagger) eller explicita konstruktorer.

Testproblem — varje test måste konfigurera ServiceLocator.shared med alla beroenden. Efter testet — återställa (reset()) tillståndet. Vid parallell körning av tester leder det globala tillståndet ServiceLocator.shared till race condition: ett test registrerar en mock, ett annat — får någon annans mock. Lösning: lokatorer med omfattning (en per test) eller ThreadLocal. DI löser detta problem från grunden: varje test skapar sin egen instans med mock-beroenden.

swift
// Problem med att testa Service Locator
class LoginViewModelTests: XCTestCase {
    override func setUp() {
        super.setUp()
        // Konfiguration av globalt register för test
        ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
    }

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

    func testLogin() {
        let viewModel = LoginViewModel() // använder ServiceLocator inuti
        // test...
    }
}

Alternativ till Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — ett enkelt mönster utan globalt tillstånd: fabriksklassen skapar tjänster och skickas via konstruktor. Ambient Context — ett trådsäkert alternativ för cross-cutting concerns (loggning, auktorisering). Det bästa alternativet — Constructor Injection med manuell fabrik utan DI-ramverk: explicit skapande av beroenden i fabriken med överföring via konstruktor ger DI:s tydlighet utan komplexiteten i Dagger/Swinject-konfiguration.

Vanliga frågor

Är Service Locator ett antimönster?

Många utvecklare anser Service Locator vara ett antimönster eftersom det döljer beroenden, försvårar testning och skapar dolda kopplingar mellan klasser. I prototyper, små projekt och för globala tjänster (loggning, analys) kan Service Locator dock vara befogat. Beslutet beror på sammanhanget: för en produktionsapplikation med team — DI, för en enskild utvecklare på en prototyp — Service Locator.

Hur skiljer sig Service Locator från en DI-container?

En DI-container (Dagger, Swinject) injicerar beroenden i ett objekt automatiskt — objektet vet inte om containerns existens. Service Locator — objektet begär själv beroenden från registret. En DI-container följer IoC-principen, Service Locator — bryter mot den: objektet hanterar självt hämtningen av sina beroenden. En DI-container fungerar före skapandet av objektet (via konstruktor), Service Locator — var som helst i koden.

När ska man använda Service Locator i iOS?

Service Locator är befogat i iOS för: globala tjänster (Analytics, Logger, Crashlytics), prototyper där konfiguration av Swinject är överdriven, och för enhetstester av stora legacy-moduler. För nya iOS-projekt rekommenderas Swinject eller manuell DI via konstruktor. SwiftUI med Environment — också en form av DI som undviker Service Locator.

Hur undviker man race condition i Service Locator?

Använd en trådsäker samling (NSLock i Swift, ConcurrentHashMap i Kotlin). För tester — ThreadLocal eller lokator med omfattning. Alternativ: async-local storage — tjänster bundna till korutin/aktor. Den bästa lösningen — undvik Service Locator för parallella tester och använd DI med explicit objektskapande för varje test.

Är Service Locator en singleton?

Vanligtvis implementeras Service Locator som en Singleton, men det är inte obligatoriskt. Du kan skapa en lokatorinstans för en modul (feature-scoped locator) och skicka den via konstruktor. En feature-scoped lokator löser problemet med globalt tillstånd men löser inte problemet med dolda beroenden. Ett sådant mönster kallas Ambient Context eller Scoped Locator.

Sammanfattning

  • Service Locator — centralt register för att hämta tjänster via statisk åtkomst
  • Dolda beroenden — den största nackdelen: beroenden syns inte i klassignaturen
  • Testning — svårare än DI på grund av globalt tillstånd och behov av återställning
  • Swift/Kotlin — implementering via trådsäker ordbok och reified generics
  • Alternativ — DI (Dagger Hilt, Swinject) — standard för produktionsprojekt
  • Befogat — i prototyper, för globala tjänster och legacy-projekt

Vi utvecklar en mobil applikation nyckelfärdigt

IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.

Diskutera projektet

Läs också