Service Locator — същността на модела, централен регистър и DI

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

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

Основни точки

  • 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). Резолюцията на услуга е търсене в речника по тип. 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 — реализация чрез статични свойства и thread-safe речник. Използва се регистър с 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<ServiceProtocol>() с делегат, което прави кода по-чист. Reified generics (<reified T>) заместват ObjectIdentifier — типът се получава от генерика. Kotlin локаторите често използват ConcurrentHashMap за thread-safe без изрични заключвания.

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 срещу 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 решава този проблем чрез проверки по време на компилация (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 — thread-safe алтернатива за 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?

Използвайте thread-safe колекция (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 — реализация чрез thread-safe речник и reified generics
  • Алтернатива — DI (Dagger Hilt, Swinject) — стандарт за продукционни проекти
  • Оправдан — в прототипи, за глобални услуги и наследени проекти

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

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също