Service Locator — суть паттерна, центральный реестр и DI

Автор: IT Sectr Опубликовано: 2026-02-18 Время чтения: 9 мин

Service Locator — архитектурный паттерн, предоставляющий центральный реестр (registry) сервисов. Клиентский код запрашивает сервис через статический локатор, не создавая его напрямую и не получая через конструктор. Service Locator часто рассматривается как альтернатива Dependency Injection: он проще в реализации, но скрывает зависимости и усложняет тестирование. Паттерн реализуется через глобальный Singleton-класс с регистрацией и разрешением сервисов. Подробнее — в сравнении DI и Service Locator от Martin Fowler.

Главное

  • Service Locator — центральный реестр (registry) для получения сервисов
  • Альтернатива DI — проще в реализации, но скрывает зависимости от клиента
  • Антипаттерн — многие разработчики считают Service Locator антипаттерном из-за скрытых зависимостей
  • Глобальный доступ — локатор доступен статически из любого места, без передачи через конструктор
  • Тестирование — сложнее, чем 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). Разрешение сервиса — это 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: глобальный реестр сервисов

Service Locator на Swift — реализация через статические свойства и потокобезопасный словарь. Используется реестр с ObjectIdentifier(Protocol.self) в качестве ключа и фабриками (()->Any) в качестве значений. Ленивая инициализация (lazy var) — стандартная практика: сервис создаётся при первом запросе. Swift требует явного приведения типа при 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()
    }
}

// Регистрация
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: ленивая регистрация

Service Locator на Kotlin — компактная реализация через object (Singleton) с inline-функциями reified для типобезопасности. Kotlin позволяет создать лаконичный локатор: val service by locator() с делегатом, что делает код чище. Reified generics () заменяют ObjectIdentifier — тип получается из дженерика. Kotlin-локаторы часто используют ConcurrentHashMap для потокобезопасности без явных блокировок.

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

// Регистрация
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 для тестов. Минус: если забыть переопределить локатор в тесте, тесты используют продакшен-сервисы.

Service Locator vs DI: сравнение и когда что выбирать

Явность зависимостей — главное различие. DI объявляет зависимости явно: init(service: ServiceProtocol) — любая IDE показывает зависимости класса. Service Locator скрывает их: dependencies = ServiceLocator.resolve() — скрыто внутри метода. При DI можно сразу увидеть все зависимости класса, при Service Locator нужно читать всё тело класса. Это делает код на Service Locator менее предсказуемым: изменение реестра может сломать любой класс, использующий локатор.

ХарактеристикаService LocatorDependency Injection
Наглядность зависимостейСкрыты в теле методовЯвно в конструкторе
ТестированиеНастройка глобального реестраMock в конструктор
МодульностьГлобальный реестр — не модульныйМодули с разными контейнерами
СложностьПростая реализация, 50-100 строкТребует Dagger/Swinject
Time-to-marketБыстрый стартНастройка контейнера

Когда Service Locator оправдан — прототипы и MVP (быстрый старт без конфигурации). Легаси-проекты, где DI-фреймворк добавить невозможно (сложная сборка, ограничения линтера). Инструментальные библиотеки (логирование, краш-репортинг) — они и так глобальны. Для продакшен-приложений с командой от 3 разработчиков рекомендуется DI: явные зависимости уменьшают количество ошибок при рефакторинге и упрощают ввод новых разработчиков в проект.

Проблемы Service Locator и альтернативы

Скрытые зависимости — класс, использующий ServiceLocator.resolve() внутри метода, невозможно проанализировать статически. IDE не показывает зависимости, компилятор не проверяет, зарегистрирован ли сервис. Ошибка «Service not registered» возникает только в рантайме. Рефакторинг становится опасным: удаление сервиса из реестра может сломать любой класс в приложении. DI решает эту проблему через compile-time проверки (Dagger) или явные конструкторы.

Проблема тестирования — каждый тест должен настроить ServiceLocator.shared со всеми зависимостями. После теста — сбросить (reset()) состояние. При параллельном запуске тестов глобальное состояние ServiceLocator.shared приводит к race condition: один тест регистрирует mock, другой — получает чужой mock. Решение: скоупные локаторы (один на тест) или ThreadLocal. DI решает эту проблему изначально: каждый тест создаёт свой экземпляр с mock-зависимостями.

swift
// Проблема тестирования 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 антипаттерном, потому что он скрывает зависимости, усложняет тестирование и создаёт скрытые связи между классами. Однако в прототипах, маленьких проектах и для глобальных сервисов (логирование, аналитика) Service Locator может быть оправдан. Решение зависит от контекста: для продакшен-приложения с командой — DI, для одного разработчика на прототипе — Service Locator.

Чем Service Locator отличается от DI-контейнера?

DI-контейнер (Dagger, Swinject) внедряет зависимости в объект автоматически — объект не знает о существовании контейнера. Service Locator — объект сам запрашивает зависимости из реестра. DI-контейнер соблюдает принцип IoC, Service Locator — нарушает: объект сам управляет получением своих зависимостей. DI-контейнер работает до создания объекта (через конструктор), Service Locator — в любом месте кода.

Когда использовать Service Locator в iOS?

Service Locator оправдан в iOS для: глобальных сервисов (Analytics, Logger, Crashlytics), прототипов, где настройка Swinject избыточна, и для модульных тестов крупных легаси-модулей. Для новых iOS-проектов рекомендуется Swinject или ручной DI через конструктор. SwiftUI с Environment — тоже форма DI, избегающая Service Locator.

Как избежать race condition в Service Locator?

Используйте потокобезопасную коллекцию (NSLock в Swift, ConcurrentHashMap в Kotlin). Для тестов — ThreadLocal или скоупный локатор. Альтернатива: async-local storage — сервисы привязаны к корутине/акторату. Лучшее решение — избегать Service Locator для параллельных тестов и использовать DI с явным созданием объектов для каждого теста.

Service Locator — это синглтон?

Обычно Service Locator реализуется как Singleton, но это необязательно. Можно создать экземпляр локатора для модуля (feature-scoped locator) и передавать его через конструктор. Feature-scoped локатор решает проблему глобального состояния, но не решает проблему скрытых зависимостей. Такой паттерн называют Ambient Context или Scoped Locator.

Итоги

  • Service Locator — центральный реестр для получения сервисов через статический доступ
  • Скрытые зависимости — главный недостаток: зависимости не видны в сигнатуре класса
  • Тестирование — сложнее DI из-за глобального состояния и необходимости сброса
  • Swift/Kotlin — реализация через потокобезопасный словарь и reified generics
  • Альтернатива — DI (Dagger Hilt, Swinject) — стандарт для продакшен-проектов
  • Оправдан — в прототипах, для глобальных сервисов и легаси-проектах

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также