Service Locator — esența pattern-ului, registru central și DI

Autor: IT Sectr Publicat: 2026-02-18 Timp de citire: 9 min

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 — registru central (registry) pentru obținerea serviciilor
  • Alternativă la DI — mai simplu de implementat, dar ascunde dependențele de client
  • Anti-pattern — mulți dezvoltatori consideră Service Locator un anti-pattern din cauza dependențelor ascunse
  • Acces global — locatorul este disponibil static din orice loc, fără transmitere prin constructor
  • Testare — mai dificilă decât DI: necesită configurarea locatorului global pentru fiecare test

Ce este Service Locator: esența și structura pattern-ului

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ăResponsabilitateSwift/Kotlin
ServiceLocatorAcces global la serviciiclass ServiceLocator
RegistryDepozit de fabrici/instanțe[ObjectIdentifier: Any]
ServiceImplementare 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: registru global de servicii

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 }.

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

// Î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: înregistrare întârziată

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.

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

// Î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.

Service Locator vs DI: comparație și când să alegeți

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 LocatorDependency Injection
Vizibilitatea dependențelorAscunse în corpul metodelorExplicite în constructor
TestareConfigurarea registrului globalMock în constructor
ModularitateRegistru global — nu modularModule cu containere diferite
ComplexitateImplementare simplă, 50-100 liniiNecesită Dagger/Swinject
Time-to-marketStart rapidConfigurarea 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.

Problemele Service Locator și alternative

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.

swift
// 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

Service Locator este un anti-pattern?

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.

Cu ce diferă Service Locator de un container DI?

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.

Când să folosiți Service Locator în iOS?

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.

Cum să evitați race condition în 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.

Service Locator este un singleton?

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

  • Service Locator — registru central pentru obținerea serviciilor prin acces static
  • Dependențe ascunse — principalul dezavantaj: dependențele nu sunt vizibile în semnătura clasei
  • Testare — mai dificilă decât DI din cauza stării globale și necesității de resetare
  • Swift/Kotlin — implementare prin dicționar thread-safe și reified generics
  • Alternativă — DI (Dagger Hilt, Swinject) — standard pentru proiecte de producție
  • Justificat — în prototipuri, pentru servicii globale și proiecte legacy

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.

Discutați proiectul

Citiți și