Service Locator — архитектонски образац који обезбеђује централни регистар (registry) услуга. Клијентски код захтева услугу преко статичког локатора, не креирајући је директно нити примајући је кроз конструктор. Service Locator се често посматра као алтернатива Dependency Injection: једноставнији је за имплементацију, али скрива зависности и отежава тестирање. Образац се имплементира преко глобалне Singleton класе са регистрацијом и резолуцијом услуга. Више — у поређењу DI и Service Locator од Мартина Фаулера.
Главно
Service Locator — образац који централизује креирање и пружање услуга. У основи је Singleton класа (Locator) која садржи регистар (Registry) услуга: речник где је кључ тип (или идентификатор) услуге, а вредност конкретна имплементација. Клијентски код позива ServiceLocator.resolve(ServiceProtocol.self) и добија готову инстанцу. Образац не прописује како се услуга креира — фабрика, DI контејнер или new унутар локатора.
Структура обрасца — Registry (регистар типа [String: Any]), Locator (статичка класа са register и resolve), Service (регистрована услуга). Регистар може чувати фабрике (closure/lambda за креирање објекта) или готове инстанце. Локатор може бити глобалан (један за апликацију) или опсежни (по feature/module). Резолуција услуге је претрага у речнику по типу. Swift и Kotlin користе тип као кључ преко метатипова: ObjectIdentifier(ServiceProtocol.self).
| Компонента | Одговорност | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Глобални приступ услугама | class ServiceLocator |
| Registry | Складиште фабрика/инстанци | [ObjectIdentifier: Any] |
| Service | Конкретна имплементација | NetworkService() |
Историја обрасца — Service Locator је описан у књизи Java Patterns (1998) и касније у Core J2EE Patterns (2001). Мартин Фаулер у чланку из 2004. пореди Service Locator са DI, напомињући да је Service Locator „једноставнија алтернатива, али лошија за тестирање“. У мобилном развоју, Service Locator се користио у раним Android пројектима и iOS апликацијама пре појаве Dagger и Swinject. Сада се образац чешће среће у легаси пројектима и прототиповима.
Service Locator у Swift — имплементација преко статичких својстава и thread-safe речника. Користи се регистар са ObjectIdentifier(Protocol.self) као кључем и фабрикама (()->Any) као вредностима. Лења иницијализација (lazy var) — стандардна пракса: услуга се креира при првом захтеву. Swift захтева експлицитно превођење типа при 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()
}
}
// Регистрација
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }
// Коришћење
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)
Управљање опсегом — локатор може чувати фабрике (transient — нови објекат сваки пут) или готове инстанце (singleton). За фабрике се региструје closure који се позива при сваком resolve. За singleton — closure са хватањем креиране инстанце. Додавање опсега: .transient, .singleton, .weak (слаба референца — објекат живи док неко држи референцу). Weak опсег је згодан за UIKit ViewController да би се избегле меморијске цурења при pop/dismiss.
Service Locator у Kotlin — компактна имплементација преко object (Singleton) са inline reified функцијама за типску безбедност. Kotlin омогућава креирање сажетог локатора: val service by locator<ServiceProtocol>() са делегатом, што чини код чистијим. Reified generics (<reified T>) замењују ObjectIdentifier — тип се добија из генерика. Kotlin локатори често користе ConcurrentHashMap за thread-safe без експлицитних закључавања.
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()
}
}
// Регистрација
ServiceLocator.register<ApiService> { RetrofitApiService() }
// Коришћење у класи
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Делегат за лењу резолуцију
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()
// Коришћење: val api by locator()
Service Locator у Android — среће се у старим пројектима пре појаве Dagger. Jetpack Hilt и Koin су потиснули Service Locator у Android заједници. Међутим, локатор остаје актуелан за модуларне тестове: једноставна имитација ServiceLocator са mock услугама без Hilt-а. Плус: не треба чекати компилацију Dagger за тестове. Минус: ако заборавите да замените локатор у тесту, тестови користе продукцијске услуге.
Очигледност зависности — главна разлика. DI декларише зависности експлицитно: init(service: ServiceProtocol) — било који IDE показује зависности класе. Service Locator их скрива: dependencies = ServiceLocator.resolve() — скривено унутар метода. Код DI можете одмах видети све зависности класе, код Service Locator морате читати цело тело класе. То чини код на Service Locator мање предвидивим: промена регистра може покварити било коју класу која користи локатор.
| Карактеристика | Service Locator | Dependency Injection |
|---|---|---|
| Видљивост зависности | Скривене у телу метода | Експлицитне у конструктору |
| Тестирање | Подешавање глобалног регистра | Mock у конструктору |
| Модуларност | Глобални регистар — није модуларан | Модули са различитим контејнерима |
| Комплексност | Једноставна имплементација, 50-100 линија | Захтева Dagger/Swinject |
| Time-to-market | Брз почетак | Подешавање контејнера |
Када је Service Locator оправдан — прототипови и MVP (брз почетак без конфигурације). Легаси пројекти где је немогуће додати DI оквир (комплексна компилација, линтер ограничења). Инструменталне библиотеке (логовање, crash reporting) — оне су ионако глобалне. За продукцијске апликације са тимом од 3 програмера препоручује се DI: експлицитне зависности смањују број грешака при рефакторингу и олакшавају увођење нових програмера у пројекат.
Скривене зависности — класа која користи ServiceLocator.resolve() унутар метода не може се статички анализирати. IDE не приказује зависности, компајлер не проверава да ли је услуга регистрована. Грешка „Service not registered“ се јавља само у рантајму. Рефакторинг постаје ризичан: уклањање услуге из регистра може покварити било коју класу у апликацији. DI решава овај проблем кроз проверу у време компилације (Dagger) или експлицитне конструкторе.
Проблем тестирања — сваки тест мора подесити ServiceLocator.shared са свим зависностима. Након теста — ресетовати (reset()) стање. При паралелном покретању тестова, глобално стање ServiceLocator.shared доводи до race condition: један тест региструје mock, други — добија туђи mock. Решење: локатори опсега (један по тесту) или ThreadLocal. DI решава овај проблем из корена: сваки тест креира своју инстанцу са mock зависностима.
// Проблем тестирања Service Locator
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Подешавање глобалног регистра за тест
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // користи ServiceLocator унутра
// тест...
}
}
Алтернативе Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — једноставан образац без глобалног стања: фабричка класа креира услуге и преноси се кроз конструктор. Ambient Context — thread-safe алтернатива за cross-cutting concerns (логовање, ауторизација). Најбоља алтернатива — Constructor Injection са ручном фабриком без DI оквира: експлицитно креирање зависности у фабрици са преношењем кроз конструктор даје прегледност DI без сложности подешавања Dagger/Swinject.
Често постављана питања
Многи програмери сматрају Service Locator антиобрасцем јер скрива зависности, отежава тестирање и ствара скривене везе између класа. Међутим, у прототиповима, малим пројектима и за глобалне услуге (логовање, аналитика) Service Locator може бити оправдан. Одлука зависи од контекста: за продукцијску апликацију са тимом — DI, за једног програмера на прототипу — Service Locator.
DI контејнер (Dagger, Swinject) убризгава зависности у објекат аутоматски — објекат не зна за постојање контејнера. Service Locator — објекат сам захтева зависности из регистра. DI контејнер поштује принцип IoC, Service Locator — га крши: објекат сам управља добијањем својих зависности. DI контејнер ради пре креирања објекта (кроз конструктор), Service Locator — на било ком месту у коду.
Service Locator је оправдан у iOS за: глобалне услуге (Analytics, Logger, Crashlytics), прототипове где је подешавање Swinject претерано, и за модуларне тестове великих легаси модула. За нове iOS пројекте препоручује се Swinject или ручни DI кроз конструктор. SwiftUI са Environment — такође облик DI који избегава Service Locator.
Користите thread-safe колекцију (NSLock у Swift, ConcurrentHashMap у Kotlin). За тестове — ThreadLocal или локатор опсега. Алтернатива: async-local storage — услуге везане за корутину/акторат. Најбоље решење — избегавати Service Locator за паралелне тестове и користити DI са експлицитним креирањем објеката за сваки тест.
Обично се Service Locator имплементира као Singleton, али то није обавезно. Можете креирати инстанцу локатора за модул (feature-scoped locator) и преносити је кроз конструктор. Feature-scoped локатор решава проблем глобалног стања, али не решава проблем скривених зависности. Такав образац се назива Ambient Context или Scoped Locator.
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође