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

Автор: IT Sectr Опубліковано: 2026-02-18 Час читання: 9 хв

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

Головне

  • Service Locator — центральний реєстр для отримання сервісів
  • Альтернатива DI — простіший у реалізації, але приховує залежності від клієнта
  • Антипатерн — багато розробників вважають Service Locator антипатерном через приховані залежності
  • Глобальний доступ — локатор доступний статично з будь-якого місця, без передачі через конструктор
  • Тестування — складніше, ніж DI: вимагає налаштування глобального локатора для кожного тесту

Що таке Service Locator: суть та структура патерну

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: глобальний реєстр сервісів

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)

Управління областю — локатор може зберігати фабрики (transient — новий об'єкт щоразу) або готові екземпляри (singleton). Для фабрик реєструється closure, який викликається при кожному resolve. Для singleton — closure із захопленням створеного екземпляра. Додавання областей: .transient, .singleton, .weak (слабке посилання — об'єкт живе поки хтось тримає посилання). Weak область зручна для 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()

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 з усіма залежностями. Після тесту — скинути стан. При паралельному запуску тестів глобальний стан 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

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