学习软件开发时,相信大家都曾至少有一次因为 DI、IoC、DIP 这三个术语而感到头疼。
课程或文章经常把三者放在一句话里,比如“IoC 容器通过 DI 注入依赖,从而遵守 DIP”,听多了就容易搞不清各自的含义。
先说结论:三个术语处于不同层次。DIP 是原则,也就是为什么要这样做;IoC 是实现该原则的整体方向,也就是谁拥有控制权;DI 是实现这一方向的具体技术,也就是如何注入。
读完本文,你会理解三者如何关联,也能在面试中被问到时毫不犹豫地回答。我也尽量让代码示例保持简短。
先用一句话总结三个术语
先分别记住一句话,再开始理解,会轻松很多。
- DIP(Dependency Inversion Principle,依赖倒置原则):不要依赖具体实现,而要依赖抽象(例如协议)的设计原则
- IoC(Inversion of Control,控制反转):由框架或容器,而不是自己的代码,掌控程序的控制流
- DI(Dependency Injection,依赖注入):不在内部创建所需对象,而是由外部传入的技术
三者的关系如下。
如果 DIP 是目标和原则,那么 IoC 就是实现该目标的广义概念。DI 则是实现 IoC 的具体方法之一。
简单来说,抽象层次会按 DIP > IoC > DI 的顺序逐步降低。
DIP 解决的是“依赖什么”的问题
DIP 是 SOLID 中的 D,即面向对象五大设计原则之一的依赖倒置原则。
核心有两点:高层模块不应依赖低层模块,二者都应依赖抽象。
听起来有点难,但结合代码就容易多了。
来看一个订单服务直接绑定特定支付服务商 Kakao Pay 的反面示例。
// 反面示例:直接依赖具体类
class OrderService {
private let pay = KakaoPay() // 更换支付服务商时必须拆改这里
}
以后如果改用 Naver Pay,就必须直接修改 OrderService 的代码。每增加一种支付方式,高层模块都会受到影响。
DIP 会反转这种关系。定义一个支付抽象(协议),让代码只依赖它。
protocol PayGateway { func pay(amount: Int) }
class OrderService {
private let pay: PayGateway // 依赖抽象,而不是具体类
init(pay: PayGateway) { self.pay = pay }
}
无论是 Kakao Pay 还是 Naver Pay,只要实现 PayGateway,就不需要修改 OrderService。这就是“依赖方向发生了反转”。
IoC 解决的是“谁掌握控制权”的问题
IoC,也就是控制反转,是一个更宽泛的概念。
通常,我们编写的代码会控制整个流程:创建对象、调用方法并决定执行顺序。
IoC 会反转这种控制权。不是我们调用框架,而是框架调用我们。
它也常被称为“好莱坞原则”:“不要打电话给我们,我们会打给你(Don’t call us, we’ll call you)。”
以 Swinject 这类 DI 容器为例,不是我们直接创建对象,而是容器代为创建、管理和连接对象。控制权从我们转移到了容器。
这里有一个重要点:IoC 不只有 DI。
- 框架调用回调
- 模板方法模式
- UIKit 代为调用 viewDidLoad 等生命周期方法
这些都是 IoC 的示例。DI 只是其中专门用于“注入依赖”的下位概念。
DI 解决的是“如何注入”的问题
DI,也就是依赖注入,是实现 IoC 最具代表性的技术。
在前面的 DIP 示例中,OrderService 通过初始化器接收 PayGateway,这就是 DI。也就是不在内部直接创建所需对象,而是从外部传入。
注入方式主要有三种。
| 注入方式 | 说明 | 推荐程度 |
|---|---|---|
| 构造器注入 | 通过 init 接收依赖 | 最推荐(不可变,必需依赖明确) |
| 方法注入 | 通过方法参数接收 | 适用于可选依赖 |
| 属性注入 | 创建后赋值给属性 | 会变成可选且为 var,因此不推荐 |
Swift 推荐使用初始化器注入。依赖可以用 let 固定,更安全;测试时也很容易传入伪对象(Mock)。
总结起来,三者的关系如下。
为了遵守 DIP 原则,沿着 IoC 的方向移交控制权,并使用 DI 作为具体手段。
这句话最简洁地概括了三个术语之间的关系。
常见问题总结
Q. DI 和 IoC 不是一回事吗?
不是。IoC 是更宽泛的概念,DI 是实现 IoC 的多种方法之一。所有 DI 都属于 IoC,但并非所有 IoC 都是 DI。
Q. 遵守 DIP 就会自动使用 DI 吗?
不一定。DIP 是“依赖抽象”的原则;当你从外部传入该抽象的实现时,才构成 DI。原则和技术是两回事。
Q. 不使用 Swinject 这样的库,也能使用 DI 吗?
可以。像上面的 Swift 示例一样,通过初始化器直接传入也是 DI。Swinject 和 Factory 只是自动化这一过程的工具,DI 本身不需要库也能成立。
总结一下:DIP 是为什么,也就是原则;IoC 是谁,也就是控制权;DI 是如何,也就是技术。把这三个问题分开记忆,就不会再混淆。
了解它们处于不同层次后,DI 相关代码会呈现出完全不同的样子。希望这篇文章能帮助你彻底理清这些概念。加油!

