代码中需要某个对象时,很多人会直接在使用处创建它。但一写测试就会碰壁,因为创建代码紧紧嵌在类里,根本无法替换。
即使搜索依赖注入(DI),看到的也常常只是“控制反转、IoC 容器”之类的说法,反而更容易困惑。
所以今天我们尽量用简单易懂的方式来说明。先说结论。
依赖注入(DI)归根结底,就是把原本在类内部直接创建的对象,改为在外部创建后传入。核心就是这一个移动:把对象的创建位置移到类外。DI 几乎全部的内容就是如此。
读完这篇文章,你会理解为什么需要 DI、代码会如何变化,以及为什么 Swinject 这样的 DI 库能替你完成这件事。
对象在类内部创建,会有什么问题?
假设有一个处理订单的类。这个类需要支付模块来完成支付。
最常见的新手代码大概是这样。
class OrderService {
// 在类内部直接创建支付对象
private let pay = KakaoPay()
func order() { pay.pay() }
}
问题在于,OrderService 被紧紧绑定到了 KakaoPay。
如果想把 KakaoPay 换成 Naver Pay,就得进入类内部修改创建部分。
测试时即使想传入假的支付对象也没办法,因为它已经在内部创建了真实的 KakaoPay。
也就是说,把对象创建放在类内部,会让这个类被某个具体实现牢牢束缚。
所以我们把对象创建移到了外部
解决方法比想象中简单。不必自己创建,只要在外部创建后接收即可。
class OrderService {
private let pay: Pay
// 只接收外部创建并传入的对象
init(pay: Pay) { self.pay = pay }
func order() { pay.pay() }
}
变化只有一个:直接创建消失了,改为通过构造器(初始化器)接收。
现在 OrderService 不需要知道支付方式是 KakaoPay 还是 Naver Pay,只需知道“接收一个叫 Pay 的东西并使用它”。
DI 的本质并不复杂。只是把对象创建移到类外,将决定传入什么对象的权限交给外部。
这种从外部传入对象的做法称为“注入(injection)”。注入依赖的对象,所以叫依赖注入。
只是名字听起来复杂,实际作用就是刚才看到的全部内容。
使用 DI 有什么好处?
光说有好处不够直观,所以我们整理一下实际发生了哪些变化。
- 更容易替换:即使把 KakaoPay 换成 Naver Pay,OrderService 也无需修改一行代码,只要更换传入的对象即可。
- 更方便测试:传入假的(Mock)支付对象,既不会真的扣款,又能只验证业务逻辑。
- 职责得到分离:OrderService 只专注于“处理订单”,“使用哪种支付方式”由外部决定。
第三点最让我有感触。类只专注于自己的工作后,代码确实更容易阅读了。
总结如下。
| 分类 | 在内部创建时 | 将创建移到外部时(DI) |
|---|---|---|
| 替换支付方式 | 需要修改类 | 只需替换传入的对象 |
| 测试 | 无法传入假对象 | 可以注入 Mock |
| 关注点 | 创建与逻辑混杂 | 只专注于逻辑 |
那么,为什么需要 Swinject 这样的 DI 库?
读到这里自然会产生一个疑问:“我明白要从外部传入了,但这个‘外部’由谁管理呢?”
对象不多时,手动创建并传入即可。但实际项目中往往有数百个对象,而且彼此交织。
让人逐个按照顺序创建并传入,简直就是地狱。
于是,替你完成这种“从外部创建并传入对象”的工具出现了。Swift 生态中的 Swinject 和 Factory 等 DI 容器,正是承担这一职责的工具。
注册好所需的对象后,容器会自动创建它们并传入需要的位置。需要亲自创建的事情几乎消失了。
常说的“控制反转(IoC)”也由此而来。它表示创建并连接对象的控制权,已经从你的代码转移到了容器。
换句话说,DI 是一种概念,而 DI 容器则是代替你执行这一概念的工作人员。
常见问题
Q. DI 和 IoC 是同一个概念吗? 不是。IoC(控制反转)是“交出控制权”这一更宽泛的原则,而 DI 是实现它的具体方法之一。可以把 DI 看作 IoC 的一种。
Q. 除了构造器,还有其他注入方式吗? 有。包括构造器注入、属性注入和方法注入。不过在 Swift 中推荐使用构造器注入,因为依赖可以固定为 let,值不会变化,也更方便测试。
Q. 必须使用库才算 DI 吗? 不是。刚才手动传入构造器的做法也是正统的 DI。库只是帮你把它自动化了。
今天我们把依赖注入,解释成对象创建位置的一次小小移动。
如果 DI 让你觉得难,只要记住一句话:“不要在类内部创建,而要在外部创建后传入。”记住这句话,其他内容自然就能理解。
如果你曾在测试代码面前感到无从下手,建议亲自动手改一遍今天看到的示例。这样会比只看代码理解得更加扎实。

