Service Locator — архітектурний патерн, що надає центральний реєстр сервісів. Клієнтський код запитує сервіс через статичний локатор, не створюючи його напряму та не отримуючи через конструктор. Service Locator часто розглядається як альтернатива Dependency Injection: він простіший у реалізації, але приховує залежності та ускладнює тестування. Патерн реалізується через глобальний Singleton-клас з реєстрацією та розв'язанням сервісів. Детальніше — у порівнянні DI та Service Locator від Martin Fowler.
Головне
Service Locator — патерн, що централізує створення та надання сервісів. В основі — Singleton-клас (Locator), який містить реєстр сервісів: словник, де ключ — тип (або ідентифікатор) сервісу, значення — конкретна реалізація. Клієнтський код викликає ServiceLocator.resolve(ServiceProtocol.self) та отримує готовий екземпляр. Патерн не визначає, як створюється сервіс — фабрика, DI-контейнер або new всередині локатора.
Структура патерну — Реєстр (словник типу [String: Any]), Локатор (статичний клас з register та resolve), Сервіс (сервіс, що реєструється). Реєстр може зберігати фабрики (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 — реалізація через статичні властивості та потокобезпечний словник. Використовується реєстр з 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 з делегатом, що робить код чистішим. Reified generics () замінюють ObjectIdentifier — тип отримується з дженерика. Kotlin-локатори часто використовують ConcurrentHashMap для потокобезпеки без явних блокувань.
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 вирішує цю проблему через compile-time перевірки (Dagger) або явні конструктори.
Проблема тестування — кожен тест повинен налаштувати ServiceLocator.shared з усіма залежностями. Після тесту — скинути стан. При паралельному запуску тестів глобальний стан 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 — потокобезпечна альтернатива для 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.
Використовуйте потокобезпечну колекцію (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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.