Service Locator ist ein Architekturmuster, das eine zentrale Registry von Diensten bereitstellt. Der Client-Code fordert einen Dienst über einen statischen Locator an, ohne ihn direkt zu erstellen oder über den Konstruktor zu erhalten. Service Locator wird oft als Alternative zu Dependency Injection betrachtet: Es ist einfacher zu implementieren, versteckt aber Abhängigkeiten und erschwert das Testen. Das Muster wird über eine globale Singleton-Klasse mit Registrierung und Auflösung von Diensten implementiert. Mehr Details — im Vergleich von DI und Service Locator von Martin Fowler.
Wichtige Erkenntnisse
Service Locator ist ein Muster, das die Erstellung und Bereitstellung von Diensten zentralisiert. Es basiert auf einer Singleton-Klasse (Locator), die eine Registry von Diensten enthält: ein Wörterbuch, in dem der Schlüssel der Diensttyp (oder Identifikator) und der Wert die konkrete Implementierung ist. Der Client-Code ruft ServiceLocator.resolve(ServiceProtocol.self) auf und erhält eine fertige Instanz. Das Muster schreibt nicht vor, wie der Dienst erstellt wird — Factory, DI-Container oder new innerhalb des Locators.
Struktur des Patterns — Registry (Wörterbuch vom Typ [String: Any]), Locator (statische Klasse mit register und resolve), Service (der zu registrierende Dienst). Die Registry kann Fabriken (Closures/Lambdas zum Erstellen von Objekten) oder fertige Instanzen speichern. Der Locator kann global (einer pro Anwendung) oder bereichsbezogen (pro Feature/Modul) sein. Die Dienstauflösung ist eine Suche im Wörterbuch nach Typ. Swift und Kotlin verwenden den Typ als Schlüssel über Metatypen: ObjectIdentifier(ServiceProtocol.self).
| Komponente | Verantwortung | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Globaler Zugriff auf Dienste | class ServiceLocator |
| Registry | Speicher für Fabriken/Instanzen | [ObjectIdentifier: Any] |
| Service | Konkrete Implementierung | NetworkService() |
Geschichte des Patterns — Service Locator wurde im Buch Java Patterns (1998) und später in Core J2EE Patterns (2001) beschrieben. Martin Fowlers Artikel von 2004 vergleicht Service Locator mit DI und stellt fest, dass Service Locator eine „einfachere Alternative, aber schlechter zum Testen" ist. In der mobilen Entwicklung wurde Service Locator in frühen Android-Projekten und iOS-Apps vor dem Aufkommen von Dagger und Swinject verwendet. Heute findet man das Muster häufiger in Legacy-Projekten und Prototypen.
Service Locator in Swift — Implementierung über statische Eigenschaften und ein threadsicheres Wörterbuch. Eine Registry mit ObjectIdentifier(Protocol.self) als Schlüssel und Fabriken (() -> Any) als Werten wird verwendet. Faule Initialisierung (lazy var) ist Standardpraxis: der Dienst wird bei der ersten Anfrage erstellt. Swift erfordert explizite Typumwandlung bei resolve: guard let service = locator.resolve(ServiceProtocol.self) else { return }.
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()
}
}
// Registrierung
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }
// Verwendung
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)
Bereichsverwaltung — der Locator kann Fabriken (transient — jedes Mal ein neues Objekt) oder fertige Instanzen (singleton) speichern. Für Fabriken wird ein Closure registriert, das bei jedem resolve aufgerufen wird. Für Singleton erfasst ein Closure die erstellte Instanz. Bereiche hinzufügen: .transient, .singleton, .weak (schwache Referenz — das Objekt lebt, solange jemand eine Referenz hält). Der Weak-Bereich ist praktisch für UIKit-UIViewControllers, um Lecks bei pop/dismiss zu vermeiden.
Service Locator in Kotlin — eine kompakte Implementierung über object (Singleton) mit inline-reified-Funktionen für Typsicherheit. Kotlin ermöglicht einen prägnanten Locator: val service by locator mit einem Delegaten, was den Code sauberer macht. Reified Generics () ersetzen ObjectIdentifier — der Typ wird aus dem Generic bezogen. Kotlin-Locators verwenden oft ConcurrentHashMap für Threadsicherheit ohne explizite Sperren.
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()
}
}
// Registrierung
ServiceLocator.register<ApiService> { RetrofitApiService() }
// Verwendung in Klasse
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Delegat für faule Auflösung
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()
// Verwendung: val api by locator()
Service Locator in Android — in älteren Projekten vor Dagger zu finden. Jetpack Hilt und Koin haben Service Locator in der Android-Community ersetzt. Der Locator bleibt jedoch für Unit-Tests relevant: ein einfacher ServiceLocator-Stub mit Mock-Diensten ohne Hilt. Vorteil: kein Warten auf Dagger-Kompilierung für Tests. Nachteil: wenn man vergisst, den Locator in einem Test zu überschreiben, verwenden die Tests Produktionsdienste.
Explizitheit von Abhängigkeiten — der Hauptunterschied. DI deklariert Abhängigkeiten explizit: init(service: ServiceProtocol) — jede IDE zeigt die Klassenabhängigkeiten. Service Locator versteckt sie: dependencies = ServiceLocator.resolve() — innerhalb der Methode versteckt. Mit DI können Sie sofort alle Klassenabhängigkeiten sehen; mit Service Locator müssen Sie den gesamten Klassenkörper lesen. Dies macht Service Locator-Code weniger vorhersagbar: eine Änderung der Registry kann jede Klasse brechen, die den Locator verwendet.
| Eigenschaft | Service Locator | Dependency Injection |
|---|---|---|
| Abhängigkeitssichtbarkeit | Versteckt in Methodenrümpfen | Explizit im Konstruktor |
| Testen | Globale Registry konfigurieren | Mock im Konstruktor |
| Modularität | Globale Registry — nicht modular | Module mit getrennten Containern |
| Komplexität | Einfache Implementierung, 50-100 Zeilen | Erfordert Dagger/Swinject |
| Time-to-Market | Schneller Start | Containereinrichtung erforderlich |
Wann Service Locator gerechtfertigt ist — Prototypen und MVPs (schneller Start ohne Konfiguration). Legacy-Projekte, in denen ein DI-Framework nicht hinzugefügt werden kann (komplexer Build, Linter-Einschränkungen). Instrumentierungsbibliotheken (Logging, Crash-Reporting) — sie sind bereits global. Für Produktionsanwendungen mit einem Team von 3+ Entwicklern wird DI empfohlen: explizite Abhängigkeiten reduzieren die Anzahl der Fehler beim Refactoring und vereinfachen die Einarbeitung neuer Entwickler.
Versteckte Abhängigkeiten — eine Klasse, die ServiceLocator.resolve() innerhalb einer Methode verwendet, kann nicht statisch analysiert werden. Die IDE zeigt keine Abhängigkeiten an, der Compiler prüft nicht, ob der Dienst registriert ist. Der Fehler „Service not registered" tritt nur zur Laufzeit auf. Refactoring wird gefährlich: das Entfernen eines Dienstes aus der Registry kann jede Klasse in der Anwendung brechen. DI löst dieses Problem durch Compile-Zeit-Prüfungen (Dagger) oder explizite Konstruktoren.
Testproblem — jeder Test muss ServiceLocator.shared mit allen Abhängigkeiten konfigurieren. Nach dem Test — Zustand zurücksetzen. Während der parallelen Testausführung führt der globale Zustand von ServiceLocator.shared zu Wettlaufsituationen: ein Test registriert einen Mock, ein anderer Test erhält den Mock eines anderen. Lösung: bereichsbezogene Locators (einer pro Test) oder ThreadLocal. DI löst dieses Problem von Anfang an: jeder Test erstellt seine eigene Instanz mit Mock-Abhängigkeiten.
// Problem beim Testen von Service Locator
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Einrichtung der globalen Registry für Test
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // verwendet ServiceLocator intern
// test...
}
}
Alternativen zu Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — ein einfaches Muster ohne globalen Zustand: eine Factory-Klasse erstellt Dienste und wird über den Konstruktor übergeben. Ambient Context — eine threadsichere Alternative für Querschnittsbelange (Logging, Autorisierung). Die beste Alternative ist Constructor Injection mit manueller Factory ohne DI-Framework: explizite Erstellung von Abhängigkeiten in einer Factory mit Konstruktorübergabe gibt DI-Klarheit ohne die Komplexität der Einrichtung von Dagger/Swinject.
Häufig gestellte Fragen
Viele Entwickler betrachten Service Locator als Anti-Pattern, weil es Abhängigkeiten verbirgt, das Testen erschwert und versteckte Kopplungen zwischen Klassen erzeugt. In Prototypen, kleinen Projekten und für globale Dienste (Logging, Analytik) kann Service Locator jedoch gerechtfertigt sein. Die Entscheidung hängt vom Kontext ab: für eine Produktionsanwendung mit Team — DI, für einen einzelnen Entwickler an einem Prototypen — Service Locator.
Ein DI-Container (Dagger, Swinject) injiziert Abhängigkeiten automatisch in ein Objekt — das Objekt weiß nichts von der Existenz des Containers. Service Locator — das Objekt selbst fordert Abhängigkeiten aus der Registry an. Ein DI-Container befolgt das IoC-Prinzip; Service Locator verletzt es: das Objekt verwaltet den Bezug seiner eigenen Abhängigkeiten. Ein DI-Container arbeitet vor der Objekterstellung (über den Konstruktor), Service Locator — an jeder Stelle im Code.
Service Locator ist in iOS gerechtfertigt für: globale Dienste (Analytics, Logger, Crashlytics), Prototypen, in denen die Einrichtung von Swinject übertrieben ist, und für Unit-Tests großer Legacy-Module. Für neue iOS-Projekte wird Swinject oder manuelles DI über den Konstruktor empfohlen. SwiftUI mit @Environment — ebenfalls eine Form von DI, die Service Locator vermeidet.
Verwenden Sie eine threadsichere Sammlung (NSLock in Swift, ConcurrentHashMap in Kotlin). Für Tests — ThreadLocal oder bereichsbezogenen Locator. Alternative: async-lokaler Speicher — Dienste an eine Koroutine/Actor gebunden. Die beste Lösung ist, Service Locator für parallele Tests zu vermeiden und DI mit expliziter Objekterstellung für jeden Test zu verwenden.
Service Locator wird typischerweise als Singleton implementiert, aber das ist nicht zwingend. Sie können eine Locator-Instanz für ein Modul (Feature-bezogener Locator) erstellen und über den Konstruktor übergeben. Ein Feature-bezogener Locator löst das Problem des globalen Zustands, aber nicht das Problem der versteckten Abhängigkeiten. Dieses Muster wird Ambient Context oder Scoped Locator genannt.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch