Service Locator — istota wzorca, centralny rejestr i DI

Autor: IT Sectr Opublikowano: 2026-02-18 Czas czytania: 9 min

Service Locator — wzorzec architektoniczny udostępniający centralny rejestr (registry) usług. Kod kliencki żąda usługi przez statyczny lokator, nie tworząc jej bezpośrednio ani nie otrzymując przez konstruktor. Service Locator jest często rozważany jako alternatywa dla Dependency Injection: jest prostszy w implementacji, ale ukrywa zależności i utrudnia testowanie. Wzorzec implementuje się przez globalną klasę Singleton z rejestracją i rozwiązywaniem usług. Więcej — w porównaniu DI i Service Locator od Martina Fowlera.

Najważniejsze

  • Service Locator — centralny rejestr (registry) do pobierania usług
  • Alternatywa dla DI — prostszy w implementacji, ale ukrywa zależności przed klientem
  • Antywzorzec — wielu programistów uważa Service Locator za antywzorzec z powodu ukrytych zależności
  • Globalny dostęp — lokator dostępny statycznie z dowolnego miejsca, bez przekazywania przez konstruktor
  • Testowanie — trudniejsze niż DI: wymaga konfiguracji globalnego lokatora dla każdego testu

Czym jest Service Locator: istota i struktura wzorca

Service Locator — wzorzec centralizujący tworzenie i udostępnianie usług. U podstaw leży klasa Singleton (Locator), która zawiera rejestr (Registry) usług: słownik, gdzie kluczem jest typ (lub identyfikator) usługi, a wartością konkretna implementacja. Kod kliencki wywołuje ServiceLocator.resolve(ServiceProtocol.self) i otrzymuje gotową instancję. Wzorzec nie narzuca, jak usługa jest tworzona — fabryka, DI-container lub new wewnątrz lokatora.

Struktura wzorca — Registry (rejestr typu [String: Any]), Locator (statyczna klasa z register i resolve), Service (rejestrowana usługa). Rejestr może przechowywać fabryki (closure/lambda do tworzenia obiektu) lub gotowe instancje. Lokator może być globalny (jeden na aplikację) lub zakresowy (na feature/module). Rozwiązywanie usługi to lookup w słowniku po typie. Swift i Kotlin używają typu jako klucza przez metatypy: ObjectIdentifier(ServiceProtocol.self).

KomponentOdpowiedzialnośćSwift/Kotlin
ServiceLocatorGlobalny dostęp do usługclass ServiceLocator
RegistryMagazyn fabryk/instancji[ObjectIdentifier: Any]
ServiceKonkretna implementacjaNetworkService()

Historia wzorca — Service Locator opisany w książce Java Patterns (1998) i później w Core J2EE Patterns (2001). Martin Fowler w artykule z 2004 roku porównuje Service Locator z DI, zauważając, że Service Locator to „prostsza alternatywa, ale gorsza do testowania”. W programowaniu mobilnym Service Locator był używany we wczesnych projektach Android i aplikacjach iOS przed pojawieniem się Dagger i Swinject. Obecnie wzorzec częściej występuje w projektach legacy i prototypach.

Service Locator w Swift: globalny rejestr usług

Service Locator w Swift — implementacja przez statyczne właściwości i bezpieczny wątkowo słownik. Używany jest rejestr z ObjectIdentifier(Protocol.self) jako klucz i fabrykami (()->Any) jako wartości. Leniwa inicjalizacja (lazy var) — standardowa praktyka: usługa tworzona przy pierwszym żądaniu. Swift wymaga jawnego rzutowania typu przy 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()
    }
}

// Rejestracja
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }

// Użycie
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)

Zarządzanie zakresem — lokator może przechowywać fabryki (transient — nowy obiekt za każdym razem) lub gotowe instancje (singleton). Dla fabryk rejestrowany jest closure, który jest wywoływany przy każdym resolve. Dla singleton — closure z przechwyceniem utworzonej instancji. Dodanie zakresu: .transient, .singleton, .weak (słaba referencja — obiekt żyje dopóki ktoś trzyma referencję). Zakres weak jest wygodny dla UIKit ViewController, aby uniknąć wycieków pamięci przy pop/dismiss.

Service Locator w Kotlin: leniwa rejestracja

Service Locator w Kotlin — zwarta implementacja przez object (Singleton) z inline-funkcjami reified dla bezpieczeństwa typów. Kotlin pozwala stworzyć zwięzły lokator: val service by locator<ServiceProtocol>() z delegatem, co czyni kod czystszym. Reified generics (<reified T>) zastępują ObjectIdentifier — typ jest pobierany z generyka. Lokatory w Kotlin często używają ConcurrentHashMap dla bezpieczeństwa wątkowego bez jawnych blokad.

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

// Rejestracja
ServiceLocator.register<ApiService> { RetrofitApiService() }

// Użycie w klasie
class UserRepository {
    private val api: ApiService = ServiceLocator.resolve()
    private val db: Database = ServiceLocator.resolve()
}

// Delegat dla leniwego rozwiązywania
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()
// Użycie: val api by locator()

Service Locator w Android — występuje w starszych projektach przed pojawieniem się Dagger. Jetpack Hilt i Koin wyparły Service Locator w społeczności Android. Jednak lokator pozostaje aktualny dla testów modułowych: prosta imitacja ServiceLocator z mock-ami bez Hilt. Plus: nie trzeba czekać na kompilację Dagger do testów. Minus: jeśli zapomnisz zastąpić lokator w teście, testy używają produkcyjnych usług.

Service Locator vs DI: porównanie i kiedy co wybrać

Jawność zależności — główna różnica. DI deklaruje zależności jawnie: init(service: ServiceProtocol) — każde IDE pokazuje zależności klasy. Service Locator je ukrywa: dependencies = ServiceLocator.resolve() — ukryte wewnątrz metody. Przy DI można od razu zobaczyć wszystkie zależności klasy, przy Service Locator trzeba czytać całe ciało klasy. To sprawia, że kod oparty na Service Locator jest mniej przewidywalny: zmiana rejestru może zepsuć każdą klasę używającą lokatora.

CechaService LocatorDependency Injection
Widoczność zależnościUkryte w ciele metodJawne w konstruktorze
TestowanieKonfiguracja globalnego rejestruMock w konstruktorze
ModułowośćGlobalny rejestr — nie modułowyModuły z różnymi kontenerami
ZłożonośćProsta implementacja, 50-100 liniiWymaga Dagger/Swinject
Time-to-marketSzybki startKonfiguracja kontenera

Kiedy Service Locator jest uzasadniony — prototypy i MVP (szybki start bez konfiguracji). Projekty legacy, gdzie nie można dodać frameworku DI (skomplikowana kompilacja, ograniczenia lintera). Biblioteki narzędziowe (logowanie, raportowanie błędów) — i tak są globalne. Dla aplikacji produkcyjnych z zespołem od 3 programistów zalecany jest DI: jawne zależności zmniejszają liczbę błędów przy refaktoryzacji i ułatwiają wprowadzanie nowych programistów do projektu.

Problemy Service Locator i alternatywy

Ukryte zależności — klasa używająca ServiceLocator.resolve() wewnątrz metody nie może być analizowana statycznie. IDE nie pokazuje zależności, kompilator nie sprawdza, czy usługa jest zarejestrowana. Błąd „Service not registered” występuje tylko w czasie wykonania. Refaktoryzacja staje się ryzykowna: usunięcie usługi z rejestru może zepsuć dowolną klasę w aplikacji. DI rozwiązuje ten problem poprzez sprawdzanie w czasie kompilacji (Dagger) lub jawne konstruktory.

Problem testowania — każdy test musi skonfigurować ServiceLocator.shared ze wszystkimi zależnościami. Po teście — zresetować (reset()) stan. Przy równoległym uruchamianiu testów globalny stan ServiceLocator.shared prowadzi do race condition: jeden test rejestruje mock, inny — otrzymuje cudzy mock. Rozwiązanie: lokatory zakresowe (jeden na test) lub ThreadLocal. DI rozwiązuje ten problem od podstaw: każdy test tworzy własną instancję z mock-zależnościami.

swift
// Problem testowania Service Locator
class LoginViewModelTests: XCTestCase {
    override func setUp() {
        super.setUp()
        // Konfiguracja globalnego rejestru dla testu
        ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
    }

    override func tearDown() {
        ServiceLocator.shared.reset()
        super.tearDown()
    }

    func testLogin() {
        let viewModel = LoginViewModel() // używa ServiceLocator wewnątrz
        // test...
    }
}

Alternatywy dla Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — prosty wzorzec bez globalnego stanu: klasa fabryki tworzy usługi i jest przekazywana przez konstruktor. Ambient Context — bezpieczna wątkowo alternatywa dla cross-cutting concerns (logowanie, autoryzacja). Najlepsza alternatywa — Constructor Injection z ręczną fabryką bez frameworku DI: jawne tworzenie zależności w fabryce z przekazaniem przez konstruktor daje czytelność DI bez złożoności konfiguracji Dagger/Swinject.

Często zadawane pytania

Czy Service Locator to antywzorzec?

Wielu programistów uważa Service Locator za antywzorzec, ponieważ ukrywa zależności, utrudnia testowanie i tworzy ukryte powiązania między klasami. Jednak w prototypach, małych projektach i dla globalnych usług (logowanie, analityka) Service Locator może być uzasadniony. Decyzja zależy od kontekstu: dla aplikacji produkcyjnej z zespołem — DI, dla jednego programisty nad prototypem — Service Locator.

Czym różni się Service Locator od kontenera DI?

Kontener DI (Dagger, Swinject) wstrzykuje zależności do obiektu automatycznie — obiekt nie wie o istnieniu kontenera. Service Locator — obiekt sam żąda zależności z rejestru. Kontener DI przestrzega zasady IoC, Service Locator — narusza: obiekt sam zarządza pobieraniem swoich zależności. Kontener DI działa przed utworzeniem obiektu (przez konstruktor), Service Locator — w dowolnym miejscu kodu.

Kiedy używać Service Locator w iOS?

Service Locator jest uzasadniony w iOS dla: globalnych usług (Analytics, Logger, Crashlytics), prototypów, gdzie konfiguracja Swinject jest nadmierna, oraz dla testów modułowych dużych modułów legacy. Dla nowych projektów iOS zalecany jest Swinject lub ręczny DI przez konstruktor. SwiftUI z Environment — również forma DI, unikająca Service Locator.

Jak uniknąć race condition w Service Locator?

Używaj bezpiecznej wątkowo kolekcji (NSLock w Swift, ConcurrentHashMap w Kotlin). Dla testów — ThreadLocal lub lokator zakresowy. Alternatywa: async-local storage — usługi przypisane do korutyny/aktoratu. Najlepszym rozwiązaniem jest unikanie Service Locator dla testów równoległych i używanie DI z jawnym tworzeniem obiektów dla każdego testu.

Czy Service Locator to singleton?

Zazwyczaj Service Locator implementuje się jako Singleton, ale nie jest to obowiązkowe. Można utworzyć instancję lokatora dla modułu (feature-scoped locator) i przekazywać go przez konstruktor. Lokator zakresowy (feature-scoped) rozwiązuje problem globalnego stanu, ale nie rozwiązuje problemu ukrytych zależności. Taki wzorzec nazywany jest Ambient Context lub Scoped Locator.

Podsumowanie

  • Service Locator — centralny rejestr do pobierania usług poprzez statyczny dostęp
  • Ukryte zależności — główna wada: zależności nie są widoczne w sygnaturze klasy
  • Testowanie — trudniejsze niż DI z powodu globalnego stanu i konieczności resetowania
  • Swift/Kotlin — implementacja przez bezpieczny wątkowo słownik i reified generics
  • Alternatywa — DI (Dagger Hilt, Swinject) — standard dla projektów produkcyjnych
  • Uzasadniony — w prototypach, dla globalnych usług i projektach legacy

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również