Service Locator — 模式本质、中央注册表与DI

作者: IT Sectr 发布日期: 2026-02-18 阅读时间: 9 分钟

Service Locator — 一种架构模式,提供中央服务注册表(registry)。客户端代码通过静态定位器请求服务,无需直接创建或通过构造函数接收。Service Locator通常被视为依赖注入的替代方案:实现更简单,但隐藏依赖关系并使测试更困难。该模式通过全局Singleton类实现服务的注册和解析。更多信息——参见Martin Fowler对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)或现成的实例。定位器可以是全局的(每个应用程序一个)或作用域范围的(按功能/模块)。服务解析是按类型在字典中查找。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在Dagger和Swinject出现之前被用于早期的Android项目和iOS应用程序。现在该模式更常见于遗留项目和原型中。

Swift中的Service Locator:全局服务注册表

Swift中的Service Locator — 通过静态属性和线程安全字典实现。使用以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)。对于工厂,注册一个闭包,每次resolve时调用。对于singleton——闭包捕获已创建的实例。添加作用域:.transient、.singleton、.weak(弱引用——只要有人持有引用,对象就存在)。Weak作用域适合UIKit ViewController,以避免在pop/dismiss时发生内存泄漏。

Kotlin中的Service Locator:延迟注册

Kotlin中的Service Locator — 通过object(Singleton)和reified内联函数实现类型安全的紧凑实现。Kotlin允许创建简洁的定位器:val service by locator<ServiceProtocol>()使用委托,使代码更整洁。Reified泛型(<reified T>)取代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()

Android中的Service Locator — 出现在Dagger出现之前的旧项目中。Jetpack Hilt和Koin已经在Android社区中取代了Service Locator。然而,定位器在单元测试中仍然有用:无需Hilt即可使用mock服务简单模拟ServiceLocator。优点:不需要等待Dagger编译即可进行测试。缺点:如果忘记在测试中重写定位器,测试将使用生产服务。

Service Locator与DI:比较及何时选择

依赖关系的可见性 — 主要区别。DI显式声明依赖关系:init(service: ServiceProtocol)——任何IDE都会显示类的依赖关系。Service Locator隐藏它们:dependencies = ServiceLocator.resolve()——隐藏在方法内部。使用DI您可以立即看到类的所有依赖关系,而使用Service Locator则必须读取整个类体。这使得基于Service Locator的代码更难以预测:注册表的更改可能会破坏任何使用定位器的类。

特性Service Locator依赖注入(DI)
依赖可见性隐藏在方法体内部在构造函数中显式声明
测试配置全局注册表在构造函数中注入Mock
模块化全局注册表——非模块化不同容器实现模块化
复杂度实现简单,50-100行需要Dagger/Swinject
上市时间快速启动需要配置容器

何时使用Service Locator — 原型和MVP(无需配置,快速启动)。无法添加DI框架的遗留项目(编译复杂、linter限制)。工具库(日志、崩溃报告)——它们本来就是全局的。对于拥有3名以上开发团队的生产应用程序,建议使用DI:显式的依赖关系可减少重构时的错误数量,并简化新开发人员的项目上手过程。

Service Locator的问题与替代方案

隐藏的依赖关系 — 在方法内部使用ServiceLocator.resolve()的类无法进行静态分析。IDE不显示依赖关系,编译器不检查服务是否已注册。„Service not registered“错误仅在运行时出现。重构变得危险:从注册表中删除服务可能会破坏应用程序中的任何类。DI通过编译时检查(Dagger)或显式构造函数解决了这个问题。

测试问题 — 每个测试都必须使用所有依赖项配置ServiceLocator.shared。测试后——重置(reset())状态。在并行测试执行中,ServiceLocator.shared的全局状态会导致竞态条件:一个测试注册一个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)、工厂方法、Ambient Context。工厂方法——一种没有全局状态的简单模式:工厂类创建服务并通过构造函数传递。Ambient Context——用于横切关注点(日志记录、授权)的线程安全替代方案。最佳替代方案——不使用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——在代码中的任何位置。

何时在iOS中使用Service Locator?

在iOS中,Service Locator适用于以下场景:全局服务(Analytics、Logger、Crashlytics),配置Swinject过于复杂的原型,以及大型遗留模块的单元测试。对于新的iOS项目,建议使用Swinject或通过构造函数手动DI。SwiftUI with Environment——也是一种避免Service Locator的DI形式。

如何避免Service Locator中的竞态条件?

使用线程安全的集合(Swift中的NSLock、Kotlin中的ConcurrentHashMap)。对于测试——使用ThreadLocal或作用域定位器。替代方案:异步本地存储——服务绑定到协程/参与者。最佳解决方案——避免在并行测试中使用Service Locator,并使用DI为每个测试显式创建对象。

Service Locator是单例吗?

通常Service Locator作为Singleton实现,但这不是必须的。您可以为模块创建一个定位器实例(feature-scoped locator)并通过构造函数传递它。Feature-scoped定位器解决了全局状态的问题,但不能解决隐藏依赖关系的问题。这种模式称为Ambient Context或Scoped Locator。

总结

  • Service Locator — 通过静态访问获取服务的中央注册表
  • 隐藏的依赖关系 — 主要缺点:依赖关系在类签名中不可见
  • 测试 — 由于全局状态和需要重置,测试比DI更困难
  • Swift/Kotlin — 通过线程安全字典和reified泛型实现
  • 替代方案 — DI(Dagger Hilt、Swinject)——生产项目的标准
  • 合理使用 — 在原型、全局服务和遗留项目中

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读