软件设计

Swift Service Locator 模式:DI 的替代方案还是反模式?(实战总结)

随着 iOS 应用规模不断扩大,你可能也曾因依赖管理而头疼不已。

4 分钟阅读
Swift Service Locator 模式:DI 的替代方案还是反模式?(实战总结) 封面图

随着 iOS 应用规模不断扩大,你可能也曾因依赖管理而头疼不已。

随着项目推进,你一定会遇到Swift Service Locator 模式。有人觉得它方便,也有人因为认为它是反模式而避之不及。

相比全面引入,我发现只在真正需要的地方局部使用,效果更好。

先说结论。

Service Locator 并不是 DI(Dependency Injection,依赖注入)的完整替代方案;使用不当时,它会成为隐藏依赖的反模式。它更像是一种辅助工具。

今天我会结合自己的经验,介绍这个模式是什么、为什么会受到批评,以及它在什么情况下仍然值得使用。


什么是 Service Locator 模式?

一句话来说,就是从中央注册表中取出所需对象来使用的方式。

在某处放置一个“注册表”,每次需要对象时都从那里查找并使用。

如果 Constructor Injection 是从外部注入依赖,那么 Service Locator 就是在内部直接取出依赖来使用。

从外部注入与从内部取出的区别
从外部注入与从内部取出的区别

结合代码,很快就能理解这个概念。

// 向中央注册表注册服务并取出使用的结构
final class ServiceLocator {
    static let shared = ServiceLocator()
    private var services: [String: Any] = [:]

    func register<T>(_ service: T) { services["\(T.self)"] = service }
    func resolve<T>() -> T { services["\(T.self)"] as! T }
}

在应用启动时注册一次即可。

实际使用的一方会这样取出所需依赖。

// 在视图模型内部直接“查找”依赖
final class FeedViewModel {
    private let api: APIClient = ServiceLocator.shared.resolve()
    // 即使不把 api放入构造器,也能正常工作
}

如你所见,最大的优点是构造器变得更简洁。


为什么 Service Locator 被称为反模式?

最大的原因只有一个:依赖被隐藏了

再看一遍上面的FeedViewModel代码。只看构造器,根本无法知道这个类使用了APIClient

必须打开内部实现才能看出来。在团队协作中,这会带来相当大的不便。

第二个问题是测试。

使用 Constructor Injection 时,测试只需直接传入 Mock。但 Service Locator 需要修改全局注册表状态,因此测试之间很容易相互影响。

第三个问题是运行时风险。

如果尝试取出忘记注册的依赖,应用不会在编译时发现问题,而是在运行过程中崩溃。上面示例中的强制类型转换as!正是问题所在。

总结如下。

比较项目 Constructor Injection(DI) Service Locator
依赖暴露 清晰可见 隐藏在内部
测试便利性 较低
错误发现时机 编译时 运行时
构造器简洁度 参数增多 简洁
只看构造器看不出使用了什么,这一点一直让我很在意
只看构造器看不出使用了什么,这一点一直让我很在意

那么,什么时候可以放心使用呢?

它并非绝对糟糕。我曾在以下场景中觉得很实用。

  1. 整个应用共享的单一服务(日志记录器、分析工具等)
  2. Constructor Injection 路径过深,参数不断向下传递时
  3. 逐步将 DI 引入遗留代码的过渡期

尤其第三种情况很符合实际。

一次性把 Constructor Injection 加入现有代码的所有位置,负担很重。这时先用 Service Locator 搭一座临时的桥,再逐步迁移到 Constructor Injection,会更安全。

如今,相比纯粹的 Service Locator,更多人使用Swinject或 Swift 的@Environment等 DI 容器。

它们内部同样从注册表中取出对象,但注册验证和作用域管理更完善,因此能降低风险。


常见问题

问:它和 Singleton 有什么区别?

Singleton 让某个特定对象成为全局对象,而 Service Locator 则像是存放多个对象的“仓库”。两者的性质有所不同。

问:SwiftUI 中也会使用吗?

SwiftUI 的@Environment@EnvironmentObject其实与 Service Locator 十分相似。可以说,Apple 在框架层面提供了类似的方式。

问:那么默认应该采用什么?

建议默认使用 Constructor Injection。Service Locator 只在确实需要的地方局部使用。

最后,还是把依赖画在白板上整理最快
最后,还是把依赖画在白板上整理最快

依赖管理没有唯一正确答案,但方向很明确。

尽可能让依赖显式可见,隐藏依赖的工具则只在必要的位置谨慎使用。只要做到这一点,维护就会轻松许多。

延伸阅读