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 рамка (сложна компилация, ограничения на линтер). Инструментални библиотеки (логиране, докладване на грешки) — те така или иначе са глобални. За продукционни приложения с екип от 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също