Service Locator — архитектурный паттерн, предоставляющий центральный реестр (registry) сервисов. Клиентский код запрашивает сервис через статический локатор, не создавая его напрямую и не получая через конструктор. Service Locator часто рассматривается как альтернатива Dependency Injection: он проще в реализации, но скрывает зависимости и усложняет тестирование. Паттерн реализуется через глобальный Singleton-класс с регистрацией и разрешением сервисов. Подробнее — в сравнении DI и Service Locator от Martin Fowler.
Главное
Service Locator — паттерн, централизующий создание и предоставление сервисов. В основе — Singleton-класс (Locator), который содержит реестр (Registry) сервисов: словарь, где ключ — тип (или идентификатор) сервиса, значение — конкретная реализация. Клиентский код вызывает ServiceLocator.resolve(ServiceProtocol.self) и получает готовый экземпляр. Паттерн не предписывает, как создаётся сервис — фабрика, DI-контейнер или new внутри локатора.
Структура паттерна — Registry (реестр типа [String: Any]), Locator (статический класс с register и resolve), Service (регистрируемый сервис). Реестр может хранить фабрики (closure/lambda для создания объекта) или готовые экземпляры. Локатор может быть глобальным (один на приложение) или скоупным (по feature/module). Разрешение сервиса — это lookup в словаре по типу. Swift и Kotlin используют тип как ключ через метатипы: ObjectIdentifier(ServiceProtocol.self).
| Компонент | Ответственность | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Глобальный доступ к сервисам | class ServiceLocator |
| Registry | Хранилище фабрик/экземпляров | [ObjectIdentifier: Any] |
| Service | Конкретная реализация | NetworkService() |
История паттерна — Service Locator описан в книге Java Patterns (1998) и позже в Core J2EE Patterns (2001). Martin Fowler в статье 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)
Scope управления — локатор может хранить фабрики (transient — новый объект каждый раз) или готовые экземпляры (singleton). Для фабрик регистрируется closure, который вызывается при каждом resolve. Для singleton — closure с захватом созданного экземпляра. Добавление scope: .transient, .singleton, .weak (слабая ссылка — объект живёт пока кто-то держит ссылку). Weak scope удобен для UIKit ViewController, чтобы избежать утечек при pop/dismiss.
Service Locator на Kotlin — компактная реализация через object (Singleton) с inline-функциями reified для типобезопасности. Kotlin позволяет создать лаконичный локатор: val service by locator
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<ApiService>()
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 со всеми зависимостями. После теста — сбросить (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 — потокобезопасная альтернатива для 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также