Service Locator — un pattern arhitectural care oferă un registru central (registry) de servicii. Codul client solicită un serviciu printr-un locator static, fără a-l crea direct sau a-l primi prin constructor. Service Locator este adesea considerat o alternativă la Dependency Injection: este mai simplu de implementat, dar ascunde dependențele și complică testarea. Pattern-ul se implementează printr-o clasă Singleton globală cu înregistrare și rezolvare a serviciilor. Mai multe — în comparația DI și Service Locator de Martin Fowler.
Principalul
Service Locator — un pattern care centralizează crearea și furnizarea serviciilor. La bază este o clasă Singleton (Locator) care conține un registru (Registry) de servicii: un dicționar în care cheia este tipul (sau identificatorul) serviciului, iar valoarea este implementarea concretă. Codul client apelează ServiceLocator.resolve(ServiceProtocol.self) și primește o instanță gata făcută. Pattern-ul nu prescrie cum este creat serviciul — fabrică, container DI sau new în interiorul locatorului.
Structura pattern-ului — Registry (registru de tip [String: Any]), Locator (clasă statică cu register și resolve), Service (serviciu înregistrabil). Registrul poate stoca fabrici (closure/lambda pentru crearea obiectelor) sau instanțe gata făcute. Locatorul poate fi global (unul per aplicație) sau cu scop (pe feature/modul). Rezolvarea serviciului este o căutare în dicționar după tip. Swift și Kotlin folosesc tipul ca cheie prin metatipuri: ObjectIdentifier(ServiceProtocol.self).
| Componentă | Responsabilitate | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Acces global la servicii | class ServiceLocator |
| Registry | Depozit de fabrici/instanțe | [ObjectIdentifier: Any] |
| Service | Implementare concretă | NetworkService() |
Istoria pattern-ului — Service Locator a fost descris în cartea Java Patterns (1998) și mai târziu în Core J2EE Patterns (2001). Martin Fowler într-un articol din 2004 compară Service Locator cu DI, menționând că Service Locator este „o alternativă mai simplă, dar mai proastă pentru testare”. În dezvoltarea mobilă, Service Locator era folosit în primele proiecte Android și aplicații iOS înainte de apariția Dagger și Swinject. Acum pattern-ul se întâlnește mai des în proiecte legacy și prototipuri.
Service Locator în Swift — implementare prin proprietăți statice și un dicționar thread-safe. Se folosește un registru cu ObjectIdentifier(Protocol.self) ca cheie și fabrici (()->Any) ca valori. Inițializarea întârziată (lazy var) — practică standard: serviciul este creat la prima cerere. Swift necesită convertirea explicită a tipului la 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()
}
}
// Înregistrare
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }
// Utilizare
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)
Gestionarea scopului — locatorul poate stoca fabrici (transient — un obiect nou de fiecare dată) sau instanțe gata făcute (singleton). Pentru fabrici se înregistrează un closure care este apelat la fiecare resolve. Pentru singleton — closure cu captarea instanței create. Adăugarea scopului: .transient, .singleton, .weak (referință slabă — obiectul trăiește cât timp cineva menține o referință). Scopul weak este convenabil pentru UIKit ViewController pentru a evita scurgerile de memorie la pop/dismiss.
Service Locator în Kotlin — o implementare compactă prin object (Singleton) cu funcții inline reified pentru siguranța tipurilor. Kotlin permite crearea unui locator concis: val service by locator<ServiceProtocol>() cu un delegat, ceea ce face codul mai curat. Reified generics (<reified T>) înlocuiesc ObjectIdentifier — tipul este obținut din generic. Locatoarele Kotlin folosesc adesea ConcurrentHashMap pentru siguranța firelor fără blocări explicite.
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()
}
}
// Înregistrare
ServiceLocator.register<ApiService> { RetrofitApiService() }
// Utilizare în clasă
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Delegat pentru rezolvare întârziată
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()
// Utilizare: val api by locator()
Service Locator în Android — întâlnit în proiecte vechi înainte de apariția Dagger. Jetpack Hilt și Koin au înlocuit Service Locator în comunitatea Android. Totuși, locatorul rămâne relevant pentru testele unitare: o imitație simplă ServiceLocator cu servicii mock fără Hilt. Plus: nu trebuie să așteptați compilarea Dagger pentru teste. Minus: dacă uitați să suprascrieți locatorul în test, testele folosesc servicii de producție.
Evidența dependențelor — principala diferență. DI declară dependențele explicit: init(service: ServiceProtocol) — orice IDE arată dependențele clasei. Service Locator le ascunde: dependencies = ServiceLocator.resolve() — ascuns în interiorul metodei. Cu DI puteți vedea imediat toate dependențele clasei, cu Service Locator trebuie să citiți întregul corp al clasei. Acest lucru face codul pe Service Locator mai puțin predictibil: o modificare a registrului poate strica orice clasă care folosește locatorul.
| Caracteristică | Service Locator | Dependency Injection |
|---|---|---|
| Vizibilitatea dependențelor | Ascunse în corpul metodelor | Explicite în constructor |
| Testare | Configurarea registrului global | Mock în constructor |
| Modularitate | Registru global — nu modular | Module cu containere diferite |
| Complexitate | Implementare simplă, 50-100 linii | Necesită Dagger/Swinject |
| Time-to-market | Start rapid | Configurarea containerului |
Când Service Locator este justificat — prototipuri și MVP (start rapid fără configurare). Proiecte legacy unde nu se poate adăuga un framework DI (compilare complexă, restricții de linter). Biblioteci instrumentale (logare, raportare crash) — oricum sunt globale. Pentru aplicații de producție cu o echipă de la 3 dezvoltatori, se recomandă DI: dependențele explicite reduc numărul de erori la refactorizare și simplifică introducerea noilor dezvoltatori în proiect.
Dependențe ascunse — o clasă care folosește ServiceLocator.resolve() în interiorul unei metode nu poate fi analizată static. IDE nu arată dependențele, compilatorul nu verifică dacă serviciul este înregistrat. Eroarea „Service not registered” apare doar la runtime. Refactorizarea devine periculoasă: ștergerea unui serviciu din registru poate strica orice clasă din aplicație. DI rezolvă această problemă prin verificări la compilare (Dagger) sau constructori explicite.
Problema testării — fiecare test trebuie să configureze ServiceLocator.shared cu toate dependențele. După test — resetarea (reset()) stării. La rularea paralelă a testelor, starea globală a ServiceLocator.shared duce la race condition: un test înregistrează un mock, altul — primește mock-ul altuia. Soluție: locatoare cu scop (unul per test) sau ThreadLocal. DI rezolvă această problemă de la bază: fiecare test își creează propria instanță cu dependențe mock.
// Problema testării Service Locator
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Configurarea registrului global pentru test
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // folosește ServiceLocator în interior
// test...
}
}
Alternative la Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — un pattern simplu fără stare globală: clasa fabrică creează servicii și este transmisă prin constructor. Ambient Context — o alternativă thread-safe pentru cross-cutting concerns (logare, autorizare). Cea mai bună alternativă — Constructor Injection cu fabrică manuală fără framework DI: crearea explicită a dependențelor în fabrică cu transmitere prin constructor oferă claritatea DI fără complexitatea configurării Dagger/Swinject.
Întrebări frecvente
Mulți dezvoltatori consideră Service Locator un anti-pattern deoarece ascunde dependențele, complică testarea și creează legături ascunse între clase. Totuși, în prototipuri, proiecte mici și pentru servicii globale (logare, analitică) Service Locator poate fi justificat. Decizia depinde de context: pentru o aplicație de producție cu echipă — DI, pentru un singur dezvoltator pe un prototip — Service Locator.
Containerul DI (Dagger, Swinject) injectează dependențele în obiect automat — obiectul nu știe de existența containerului. Service Locator — obiectul însuși solicită dependențe din registru. Containerul DI respectă principiul IoC, Service Locator — îl încalcă: obiectul însuși gestionează obținerea dependențelor sale. Containerul DI funcționează înainte de crearea obiectului (prin constructor), Service Locator — în orice loc din cod.
Service Locator este justificat în iOS pentru: servicii globale (Analytics, Logger, Crashlytics), prototipuri unde configurarea Swinject este excesivă, și pentru testele unitare ale modulelor legacy mari. Pentru proiecte iOS noi se recomandă Swinject sau DI manual prin constructor. SwiftUI cu Environment — de asemenea o formă de DI care evită Service Locator.
Folosiți o colecție thread-safe (NSLock în Swift, ConcurrentHashMap în Kotlin). Pentru teste — ThreadLocal sau locator cu scop. Alternativă: async-local storage — servicii legate de corutină/actor. Cea mai bună soluție — evitați Service Locator pentru teste paralele și folosiți DI cu crearea explicită a obiectelor pentru fiecare test.
De obicei, Service Locator este implementat ca Singleton, dar acest lucru nu este obligatoriu. Puteți crea o instanță de locator pentru un modul (feature-scoped locator) și o transmiteți prin constructor. Locatorul cu scop (feature-scoped) rezolvă problema stării globale, dar nu rezolvă problema dependențelor ascunse. Un astfel de pattern se numește Ambient Context sau Scoped Locator.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și