从今天起,我们将开启一个全新的技术分享系列------深入浅出依赖注入(Dependency Injection, DI)。我希望通过这个系列,能和大家一起探讨如何编写出更健壮、更灵活、更易于测试的代码,最终提升我们整个团队的工程质量和开发效率。
我们都可能遇到过这样的场景:接手一个"祖传"模块,想修改一个小小的功能,却发现牵一发而动全身。代码像一碗缠绕不清的意大利面条,让你无从下手。这种高耦合、职责不清的代码,正是我们要通过引入DI等优秀设计思想来解决的头号敌人。
今天,作为开篇,我们先不谈高深的框架和理论,就从一个我们最熟悉的痛点开始。
1. 我们的痛点:一个典型的OrderViewController
想象一下,我们有一个负责展示订单详情的页面 OrderViewController
。它的代码可能是这样的:
swift
// A "traditional" view controller
class OrderViewController: UIViewController {
private let networkService: NetworkService
private let databaseManager: DatabaseManager
private let analyticsTracker: AnalyticsTracker
// ... 其他属性
init(orderId: String) {
// 问题在这里!
// ViewController 亲自创建了自己的依赖
self.networkService = NetworkService() // 依赖1:网络服务
self.databaseManager = DatabaseManager.shared // 依赖2:数据库管理器(单例)
self.analyticsTracker = AnalyticsTracker(trackingId: "XYZ-123") // 依赖3:分析追踪器
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
fetchOrderDetails()
}
private func fetchOrderDetails() {
networkService.fetchOrder { [weak self] order in
self?.databaseManager.save(order)
self?.analyticsTracker.trackEvent("order_displayed")
// ... 更新UI
}
}
// ... 其他代码
}
这段代码看起来很"正常",但它隐藏着几个严重的问题:
- 极难测试: 你想为
OrderViewController
写个单元测试,测试获取订单后的UI展现逻辑。但只要你创建它,它就会创建一个真实的NetworkService
并可能发出真实的网络请求。你想用一个模拟的MockNetworkService
来返回假数据吗?对不起,做不到,因为NetworkService
的创建被写死在了init
方法内部。 - 缺乏灵活性: 某天,产品经理说:"对于VIP用户的订单,我们要用一个新的
VipNetworkService
来请求,它有不同的缓存策略"。怎么办?你只能去修改OrderViewController
的内部代码,增加if/else
逻辑。如果未来还有SVIPNetworkService
呢?这个类会变得越来越臃肿。 - 依赖关系模糊: 如果不读
init
的实现,单从类的定义看,你根本不知道OrderViewController
到底需要哪些"帮手"(依赖)才能正常工作。这些依赖关系像"幽灵"一样隐藏在实现细节里。
2. 核心思想:从"我主动去拿"到"你从外部给我"
以上问题的根源在于:OrderViewController
主动承担了创建其依赖(NetworkService
等)的责任。
这就像一个厨师,不仅要负责炒菜,还得自己去种菜、养猪、打酱油。这显然不合理。专业的厨师只需要告诉采购员:"我需要顶级的牛肉和新鲜的番茄",他拿到食材后直接开始烹饪即可。
控制反转 (Inversion of Control, IoC) 就是这个思想。它将"创建依赖"这个控制权,从类的内部"反转"到了类的外部。
而 依赖注入 (Dependency Injection, DI) 则是实现IoC最常见、最直接的方式。它的口号就是:"别来问我需要什么,直接把你给我的东西拿来用"。
3. DI的三种基本姿势(Swift版)
那么,外部如何把依赖"喂"给我们的类呢?通常有三种方式:
a. 构造函数注入 (Initializer Injection) - 我们的首选!
这是最推荐、最纯粹的DI方式。我们将所有必需的依赖都通过init
方法传入。
swift
class OrderViewController: UIViewController {
private let networkService: NetworkServiceProtocol
private let databaseManager: DatabaseManagerProtocol
private let analyticsTracker: AnalyticsTrackerProtocol
// 依赖通过构造函数"注入"
init(orderId: String,
networkService: NetworkServiceProtocol,
databaseManager: DatabaseManagerProtocol,
analyticsTracker: AnalyticsTrackerProtocol) {
self.networkService = networkService
self.databaseManager = databaseManager
self.analyticsTracker = analyticsTracker
super.init(nibName: nil, bundle: nil)
}
// ...
}
// 在创建它的时候,由外部决定给它什么样的实例
let realService = NetworkService()
let mockService = MockNetworkService() // 用于测试
let vcForProd = OrderViewController(orderId: "123", networkService: realService, ...)
let vcForTest = OrderViewController(orderId: "123", networkService: mockService, ...)
优点:
- 依赖明确: 一眼就能看出这个类需要哪些依赖才能工作。
- 保证可用: 实例在创建完成后,所有依赖都已就绪,且不可更改(因为我们用了
let
)。 - 非常利于测试: 像上面例子一样,轻松传入Mock对象。
b. 属性注入 (Property Injection)
通过一个可变的属性来注入依赖。
swift
class OrderViewController: UIViewController {
var networkService: NetworkServiceProtocol! // 注意这里的 '!'
// ...
}
let vc = OrderViewController(orderId: "123")
vc.networkService = NetworkService() // 在创建后,通过属性设置依赖
适用场景:
- 当框架负责创建对象时,比如从Storyboard或XIB初始化的ViewController,我们无法干预其
init
方法。 - 用于解决循环依赖(我们将在后续文章中深入探讨)。
缺点: 依赖是可变的,且在对象生命周期的某个时刻可能是nil
,不够安全。
c. 方法注入 (Method Injection)
只在调用某个特定方法时,才把依赖传进去。
swift
class OrderLogger {
func logOrder(_ order: Order, using persister: PersistenceProtocol) {
let logData = format(order)
persister.save(data: logData)
}
}
适用场景: 当这个依赖(persister
)与类的核心职责关系不大,仅仅是某一个操作需要时,使用方法注入可以避免让整个类都持有它。
4. 重点辨析:依赖注入 vs. 服务发现 (Service Locator)
这是新手中最常见的误区。很多人会把"服务发现"模式误当成DI,因为它看起来也能解决"硬编码创建实例"的问题。
服务发现模式通常长这样:
swift
// 一个全局的服务定位器
class ServiceLocator {
static let shared = ServiceLocator()
private lazy var networkService: NetworkServiceProtocol = NetworkService()
func getService<T>() -> T? {
if T.self == NetworkServiceProtocol.self {
return networkService as? T
}
return nil
}
}
// 在ViewController中使用
class OrderViewController: UIViewController {
private let networkService: NetworkServiceProtocol
init(orderId: String) {
// 主动去全局的"电话簿"查询服务
guard let service = ServiceLocator.shared.getService() else {
fatalError("NetworkService not found!")
}
self.networkService = service
// ...
}
}
看起来不错,但它其实是"披着羊皮的狼",会带来新的问题:
- 隐藏的依赖(幽灵依赖):
OrderViewController
的依赖关系又被隐藏起来了。它的init
签名没有告诉我们它需要NetworkService
,我们必须深入其代码,去寻找ServiceLocator.shared
的调用。这让代码的可读性和可维护性急剧下降。 - 全局状态与耦合:
ServiceLocator
是一个全局单例。任何地方都可以访问它,任何地方也可能修改它。你的类不再是独立的,而是和一个看不见的全局状态紧紧地绑在了一起。 - 测试的痛苦: 当你测试
OrderViewController
时,你不能简单地传入一个mock对象了。你必须去处理那个全局的ServiceLocator
,比如在测试开始前注册一个mock服务,在测试结束后再清理掉。这非常麻烦,且容易导致测试用例之间互相干扰。
一句话总结:
- 依赖注入(DI): 依赖关系是明确的(Explicit)。类在"被动"地接收依赖。
- 服务发现(Service Locator): 依赖关系是模糊的(Implicit)。类在"主动"地查询依赖。
作为团队,我们应该始终追求明确的、可预测的代码,因此,依赖注入是远优于服务发现的选择。
总结与展望
今天我们理清了依赖注入的核心思想:通过将依赖的创建权交由外部,实现类与类之间的解耦,从而极大地提升代码的可测试性、灵活性和可维护性。 我们学习了三种注入方式,并重点辨析了DI与服务发现的本质区别。
现在,你可能会想:"如果我的应用有几十上百个依赖,难道都要在AppDelegate
里手动创建和注入吗?那也太复杂了!"
你问到了点子上!这正是DI容器(DI Container)要解决的问题。
在下一篇文章中,我们将亲自动手,从零到一写一个我们自己的迷你DI容器。通过这个过程,你将彻底理解那些流行的DI框架(如Swinject)背后的魔法究竟是什么。
敬请期待!