Service Locator — egy architekturális minta, amely központi szolgáltatásjegyzéket (registry) biztosít. A klienskód egy statikus lokátoron keresztül kér egy szolgáltatást, anélkül hogy közvetlenül létrehozná vagy konstruktoron keresztül kapná. A Service Locator-t gyakran a Dependency Injection alternatívájának tekintik: egyszerűbb implementálni, de elrejti a függőségeket és megnehezíti a tesztelést. A minta egy globális Singleton osztályon keresztül valósul meg szolgáltatások regisztrálásával és feloldásával. Bővebben — a Martin Fowler-féle DI és Service Locator összehasonlításban.
Főbb pontok
Service Locator — egy minta, amely központosítja a szolgáltatások létrehozását és biztosítását. Az alap egy Singleton osztály (Locator), amely szolgáltatások jegyzékét (Registry) tartalmazza: egy szótár, ahol a kulcs a szolgáltatás típusa (vagy azonosítója), az érték pedig a konkrét implementáció. A klienskód meghívja a ServiceLocator.resolve(ServiceProtocol.self)-t és egy kész példányt kap. A minta nem írja elő, hogyan jön létre a szolgáltatás — gyár, DI konténer vagy new a lokátoron belül.
A minta szerkezete — Registry ([String: Any] típusú jegyzék), Locator (statikus osztály register és resolve metódusokkal), Service (regisztrálható szolgáltatás). A jegyzék tárolhat gyárakat (closure/lambda objektum létrehozásához) vagy kész példányokat. A lokátor lehet globális (egy az alkalmazáshoz) vagy hatókörrel rendelkező (feature/modul szerint). A szolgáltatás feloldása egy keresés a szótárban típus alapján. A Swift és Kotlin a típust használja kulcsként metatípusokon keresztül: ObjectIdentifier(ServiceProtocol.self).
| Összetevő | Felelősség | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Globális hozzáférés szolgáltatásokhoz | class ServiceLocator |
| Registry | Gyárak/példányok tárolása | [ObjectIdentifier: Any] |
| Service | Konkrét implementáció | NetworkService() |
A minta története — A Service Locator-t a Java Patterns (1998) könyv írta le, később a Core J2EE Patterns (2001) könyvben is megjelent. Martin Fowler 2004-es cikkében hasonlítja össze a Service Locator-t a DI-val, megjegyezve, hogy a Service Locator „egyszerűbb alternatíva, de rosszabb a teszteléshez“. A mobilfejlesztésben a Service Locator-t korai Android-projektekben és iOS-alkalmazásokban használták a Dagger és Swinject megjelenése előtt. Ma a minta gyakrabban fordul elő legacy projektekben és prototípusokban.
Service Locator Swift-ben — implementáció statikus tulajdonságokon és thread-safe szótáron keresztül. Egy jegyzék kerül használatra ObjectIdentifier(Protocol.self) kulccsal és (()->Any) gyárakkal értékként. Lusta inicializálás (lazy var) — általános gyakorlat: a szolgáltatás az első kéréskor jön létre. A Swift explicit típuskonverziót igényel resolve-kor: 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()
}
}
// Regisztráció
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }
// Használat
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)
Hatókör kezelés — a lokátor tárolhat gyárakat (transient — minden alkalommal új objektum) vagy kész példányokat (singleton). Gyárak esetén egy closure kerül regisztrálásra, amely minden resolve-kor meghívódik. Singleton esetén — closure a létrehozott példány rögzítésével. Hatókör hozzáadása: .transient, .singleton, .weak (gyenge referencia — az objektum addig él, amíg valaki tartja a referenciát). A weak hatókör kényelmes UIKit ViewController esetén a memóriaszivárgás elkerülésére pop/dismiss esetén.
Service Locator Kotlin-ban — tömör implementáció object (Singleton) segítségével reified inline függvényekkel a típusbiztonságért. A Kotlin lehetővé teszi egy tömör lokátor létrehozását: val service by locator<ServiceProtocol>() delegálással, ami tisztábbá teszi a kódot. A Reified generics (<reified T>) helyettesíti az ObjectIdentifier-t — a típus a generikusból származik. A Kotlin lokátorok gyakran használnak ConcurrentHashMap-t a szálbiztonság érdekében explicit zárolások nélkül.
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()
}
}
// Regisztráció
ServiceLocator.register<ApiService> { RetrofitApiService() }
// Használat osztályban
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Delegált lusta feloldáshoz
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()
// Használat: val api by locator()
Service Locator Android-ban — régi projektekben fordul elő a Dagger megjelenése előtt. A Jetpack Hilt és Koin kiszorította a Service Locator-t az Android közösségből. A lokátor azonban továbbra is releváns az egységtesztekhez: egyszerű ServiceLocator imitáció mock szolgáltatásokkal Hilt nélkül. Plusz: nem kell várni a Dagger fordítására a tesztekhez. Mínusz: ha elfelejti felülírni a lokátort a tesztben, a tesztek éles szolgáltatásokat használnak.
Függőségek átláthatósága — a fő különbség. A DI explicit módon deklarálja a függőségeket: init(service: ServiceProtocol) — bármely IDE mutatja az osztály függőségeit. A Service Locator elrejti őket: dependencies = ServiceLocator.resolve() — rejtve a metóduson belül. DI esetén azonnal láthatja az osztály összes függőségét, Service Locator esetén el kell olvasnia az osztály teljes törzsét. Ez kevésbé kiszámíthatóvá teszi a Service Locator kódot: a jegyzék megváltoztatása bármely, a lokátort használó osztályt elronthat.
| Jellemző | Service Locator | Dependency Injection |
|---|---|---|
| Függőségek láthatósága | Rejtve a metódusok törzsében | Explicit a konstruktorban |
| Tesztelés | Globális jegyzék konfigurálása | Mock a konstruktorban |
| Modularitás | Globális jegyzék — nem moduláris | Modulok különböző konténerekkel |
| Komplexitás | Egyszerű implementáció, 50-100 sor | Dagger/Swinject szükséges |
| Time-to-market | Gyors indulás | Konténer konfigurálása |
Mikor indokolt a Service Locator — prototípusok és MVP (gyors indulás konfiguráció nélkül). Legacy projektek, ahol nem lehet DI keretrendszert hozzáadni (bonyolult fordítás, lint korlátozások). Eszközkönyvtárak (naplózás, hibajelentés) — ezek úgyis globálisak. Éles alkalmazásokhoz 3 fejlesztőből álló csapattal a DI ajánlott: az explicit függőségek csökkentik a hibák számát refaktoráláskor és megkönnyítik az új fejlesztők bevezetését a projektbe.
Rejtett függőségek — egy osztály, amely ServiceLocator.resolve()-t használ egy metóduson belül, nem elemezhető statikusan. Az IDE nem mutatja a függőségeket, a fordító nem ellenőrzi, hogy a szolgáltatás regisztrálva van-e. A „Service not registered” hiba csak futásidőben jelentkezik. A refaktorálás veszélyessé válik: egy szolgáltatás eltávolítása a jegyzékből bármely osztályt elronthat az alkalmazásban. A DI ezt a problémát fordítási időben történő ellenőrzésekkel (Dagger) vagy explicit konstruktorokkal oldja meg.
Tesztelési probléma — minden tesztnek konfigurálnia kell a ServiceLocator.shared-et az összes függőséggel. A teszt után — vissza kell állítani (reset()) az állapotot. A tesztek párhuzamos futtatásakor a ServiceLocator.shared globális állapota versenyhelyzethez (race condition) vezet: az egyik teszt regisztrál egy mock-ot, egy másik — egy másik mock-ját kapja. Megoldás: hatókörrel rendelkező lokátorok (egy tesztre egy) vagy ThreadLocal. A DI ezt a problémát gyökeresen oldja meg: minden teszt létrehozza a saját példányát mock függőségekkel.
// A Service Locator tesztelésének problémája
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Globális jegyzék konfigurálása a teszthez
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // ServiceLocator-t használ belül
// teszt...
}
}
A Service Locator alternatívái — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — egyszerű minta globális állapot nélkül: a gyár osztály létrehozza a szolgáltatásokat és konstruktoron keresztül kerül átadásra. Ambient Context — thread-safe alternatíva keresztmetszeti aggályokhoz (naplózás, engedélyezés). A legjobb alternatíva — Constructor Injection kézi gyárral DI keretrendszer nélkül: a függőségek explicit létrehozása a gyárban konstruktoron keresztüli átadással megadja a DI átláthatóságát a Dagger/Swinject konfiguráció bonyolultsága nélkül.
Gyakran Ismételt Kérdések
Sok fejlesztő antimintának tartja a Service Locator-t, mert elrejti a függőségeket, megnehezíti a tesztelést és rejtett kapcsolatokat hoz létre az osztályok között. Azonban prototípusokban, kis projektekben és globális szolgáltatások (naplózás, analitika) esetén a Service Locator indokolt lehet. A döntés a kontextustól függ: éles alkalmazáshoz csapattal — DI, egy fejlesztő prototípusához — Service Locator.
A DI konténer (Dagger, Swinject) automatikusan injektálja a függőségeket egy objektumba — az objektum nem tud a konténer létezéséről. Service Locator — az objektum maga kéri a függőségeket a jegyzékből. A DI konténer betartja az IoC elvet, a Service Locator — megsérti: az objektum maga kezeli a függőségei megszerzését. A DI konténer az objektum létrehozása előtt működik (konstruktoron keresztül), a Service Locator — a kód bármely pontján.
A Service Locator iOS-ben indokolt: globális szolgáltatásokhoz (Analytics, Logger, Crashlytics), prototípusokhoz ahol a Swinject konfigurálása túlzás, és nagy legacy modulok egységtesztjeihez. Új iOS projektekhez a Swinject vagy a kézi DI konstruktoron keresztül ajánlott. A SwiftUI Environment-nel — szintén a DI egy formája, amely elkerüli a Service Locator-t.
Használjon szálbiztos gyűjteményt (NSLock Swift-ben, ConcurrentHashMap Kotlin-ban). Tesztekhez — ThreadLocal vagy hatókörrel rendelkező lokátor. Alternatíva: async-local storage — szolgáltatások korutinhoz/aktorhoz kötve. A legjobb megoldás — kerülje a Service Locator-t párhuzamos tesztekhez, és használjon DI-t explicit objektumlétrehozással minden teszthez.
Általában a Service Locator Singleton-ként van implementálva, de ez nem kötelező. Létrehozhat egy lokátor példányt egy modulhoz (feature-scoped locator) és átadhatja konstruktoron keresztül. A feature-scoped lokátor megoldja a globális állapot problémáját, de nem oldja meg a rejtett függőségek problémáját. Az ilyen mintát Ambient Context vagy Scoped Locator néven ismerik.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is