Service Locator — architektonický vzor poskytující centrální registr (registry) služeb. Klientský kód požaduje službu přes statický lokátor, aniž by ji vytvářel přímo nebo přijímal přes konstruktor. Service Locator je často považován za alternativu k Dependency Injection: je jednodušší na implementaci, ale skrývá závislosti a ztěžuje testování. Vzor je implementován přes globální třídu Singleton s registrací a rozlišením služeb. Více — v srovnání DI a Service Locator od Martina Fowlera.
Hlavní
Service Locator — vzor centralizující vytváření a poskytování služeb. V základu je třída Singleton (Locator), která obsahuje registr (Registry) služeb: slovník, kde klíčem je typ (nebo identifikátor) služby a hodnotou konkrétní implementace. Klientský kód volá ServiceLocator.resolve(ServiceProtocol.self) a obdrží hotovou instanci. Vzor nepředepisuje, jak je služba vytvářena — továrna, DI kontejner nebo new uvnitř lokátoru.
Struktura vzoru — Registry (registr typu [String: Any]), Locator (statická třída s register a resolve), Service (registrovatelná služba). Registr může ukládat továrny (closure/lambda pro vytvoření objektu) nebo hotové instance. Lokátor může být globální (jeden na aplikaci) nebo s rozsahem (na feature/modul). Rozlišení služby je vyhledávání ve slovníku podle typu. Swift a Kotlin používají typ jako klíč přes metatypy: ObjectIdentifier(ServiceProtocol.self).
| Komponenta | Odpovědnost | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Globální přístup ke službám | class ServiceLocator |
| Registry | Úložiště továren/instancí | [ObjectIdentifier: Any] |
| Service | Konkrétní implementace | NetworkService() |
Historie vzoru — Service Locator byl popsán v knize Java Patterns (1998) a později v Core J2EE Patterns (2001). Martin Fowler v článku z roku 2004 srovnává Service Locator s DI a poznamenává, že Service Locator je „jednodušší alternativa, ale horší pro testování“. V mobilním vývoji byl Service Locator používán v raných Android projektech a iOS aplikacích před příchodem Dagger a Swinject. Nyní se vzor vyskytuje častěji v legacy projektech a prototypech.
Service Locator ve Swift — implementace přes statické vlastnosti a thread-safe slovník. Používá se registr s ObjectIdentifier(Protocol.self) jako klíčem a továrnami (()->Any) jako hodnotami. Líná inicializace (lazy var) — standardní praxe: služba je vytvořena při prvním požadavku. Swift vyžaduje explicitní převod typu při 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()
}
}
// Registrace
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }
// Použití
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)
Správa rozsahu — lokátor může ukládat továrny (transient — pokaždé nový objekt) nebo hotové instance (singleton). Pro továrny je registrován closure, který je volán při každém resolve. Pro singleton — closure se zachycením vytvořené instance. Přidání rozsahu: .transient, .singleton, .weak (slabá reference — objekt žije dokud někdo drží referenci). Weak rozsah je vhodný pro UIKit ViewController, aby se předešlo únikům paměti při pop/dismiss.
Service Locator v Kotlin — kompaktní implementace přes object (Singleton) s inline reified funkcemi pro typovou bezpečnost. Kotlin umožňuje vytvoření stručného lokátoru: val service by locator<ServiceProtocol>() s delegátem, což činí kód čistším. Reified generics (<reified T>) nahrazují ObjectIdentifier — typ je získán z generika. Kotlinové lokátory často používají ConcurrentHashMap pro thread-safety bez explicitních zámků.
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()
}
}
// Registrace
ServiceLocator.register<ApiService> { RetrofitApiService() }
// Použití ve třídě
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Delegát pro líné rozlišení
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()
// Použití: val api by locator()
Service Locator v Android — vyskytuje se ve starých projektech před příchodem Dagger. Jetpack Hilt a Koin vytlačily Service Locator v Android komunitě. Lokátor však zůstává relevantní pro unit testy: jednoduchá imitace ServiceLocator s mock službami bez Hilt. Plus: není třeba čekat na kompilaci Dagger pro testy. Mínus: pokud zapomenete přepsat lokátor v testu, testy používají produkční služby.
Viditelnost závislostí — hlavní rozdíl. DI deklaruje závislosti explicitně: init(service: ServiceProtocol) — jakékoli IDE zobrazí závislosti třídy. Service Locator je skrývá: dependencies = ServiceLocator.resolve() — skryto uvnitř metody. S DI můžete okamžitě vidět všechny závislosti třídy, s Service Locator musíte číst celé tělo třídy. To činí kód na Service Locator méně předvídatelným: změna registru může rozbít jakoukoli třídu používající lokátor.
| Vlastnost | Service Locator | Dependency Injection |
|---|---|---|
| Viditelnost závislostí | Skryté v těle metod | Explicitní v konstruktoru |
| Testování | Konfigurace globálního registru | Mock v konstruktoru |
| Modularita | Globální registr — není modulární | Moduly s různými kontejnery |
| Složitost | Jednoduchá implementace, 50-100 řádků | Vyžaduje Dagger/Swinject |
| Time-to-market | Rychlý start | Konfigurace kontejneru |
Kdy je Service Locator oprávněný — prototypy a MVP (rychlý start bez konfigurace). Legacy projekty, kde nelze přidat DI framework (složitá kompilace, omezení linteru). Instrumentální knihovny (logování, hlášení chyb) — jsou stejně globální. Pro produkční aplikace s týmem od 3 vývojářů se doporučuje DI: explicitní závislosti snižují počet chyb při refaktorování a usnadňují uvedení nových vývojářů do projektu.
Skryté závislosti — třídu používající ServiceLocator.resolve() uvnitř metody nelze staticky analyzovat. IDE nezobrazuje závislosti, kompilátor nekontroluje, zda je služba zaregistrována. Chyba „Service not registered“ nastává pouze za běhu. Refaktorování se stává riskantním: odstranění služby z registru může rozbít jakoukoli třídu v aplikaci. DI řeší tento problém pomocí kontrol v čase kompilace (Dagger) nebo explicitních konstruktorů.
Problém testování — každý test musí nakonfigurovat ServiceLocator.shared se všemi závislostmi. Po testu — resetovat (reset()) stav. Při paralelním spouštění testů vede globální stav ServiceLocator.shared k race condition: jeden test registruje mock, druhý — obdrží cizí mock. Řešení: lokátory s rozsahem (jeden na test) nebo ThreadLocal. DI řeší tento problém od základu: každý test si vytváří vlastní instanci s mock závislostmi.
// Problém testování Service Locator
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Konfigurace globálního registru pro test
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // používá ServiceLocator uvnitř
// test...
}
}
Alternativy k Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — jednoduchý vzor bez globálního stavu: tovární třída vytváří služby a je předávána přes konstruktor. Ambient Context — thread-safe alternativa pro cross-cutting concerns (logování, autorizace). Nejlepší alternativa — Constructor Injection s ruční továrnou bez DI frameworku: explicitní vytváření závislostí v továrně s předáním přes konstruktor poskytuje přehlednost DI bez složitosti konfigurace Dagger/Swinject.
Často kladené otázky
Mnoho vývojářů považuje Service Locator za antivzor, protože skrývá závislosti, ztěžuje testování a vytváří skryté vazby mezi třídami. Nicméně v prototypech, malých projektech a pro globální služby (logování, analýza) může být Service Locator oprávněný. Rozhodnutí závisí na kontextu: pro produkční aplikaci s týmem — DI, pro jednoho vývojáře na prototypu — Service Locator.
DI kontejner (Dagger, Swinject) vkládá závislosti do objektu automaticky — objekt neví o existenci kontejneru. Service Locator — objekt sám požaduje závislosti z registru. DI kontejner dodržuje princip IoC, Service Locator — ho porušuje: objekt sám řídí získávání svých závislostí. DI kontejner pracuje před vytvořením objektu (přes konstruktor), Service Locator — na libovolném místě kódu.
Service Locator je oprávněný v iOS pro: globální služby (Analytics, Logger, Crashlytics), prototypy, kde je konfigurace Swinject nadměrná, a pro unit testy velkých legacy modulů. Pro nové iOS projekty se doporučuje Swinject nebo ruční DI přes konstruktor. SwiftUI s Environment — také forma DI, která se vyhýbá Service Locator.
Použijte thread-safe kolekci (NSLock ve Swift, ConcurrentHashMap v Kotlin). Pro testy — ThreadLocal nebo lokátor s rozsahem. Alternativa: async-local storage — služby vázané na korutinu/aktor. Nejlepší řešení — vyhněte se Service Locator pro paralelní testy a použijte DI s explicitním vytvářením objektů pro každý test.
Obvykle je Service Locator implementován jako Singleton, ale není to povinné. Můžete vytvořit instanci lokátoru pro modul (feature-scoped locator) a předávat ji přes konstruktor. Feature-scoped lokátor řeší problém globálního stavu, ale neřeší problém skrytých závislostí. Takový vzor se nazývá Ambient Context nebo Scoped Locator.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také