软件设计

DI、IoC、DIP 的区别:一次彻底讲清三个术语

DI、IoC、DIP 是处于不同层次的概念。DIP 是决定依赖什么的原则,IoC 关注谁掌握控制权,DI 则是注入依赖的技术。本文结合示例和面试回答,完整梳理三者的区别。

5 分钟阅读
DI、IoC、DIP 的区别:一次彻底讲清三个术语 封面图

学习软件开发时,相信大家都曾至少有一次因为 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 经过 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 只是其中专门用于“注入依赖”的下位概念。

桌上放着一部老式电话,旁边有一张写着“Don't call us, we'll call you”的卡片
控制权发生转移,这就是 IoC 的全部含义

DI 解决的是“如何注入”的问题

DI,也就是依赖注入,是实现 IoC 最具代表性的技术。

在前面的 DIP 示例中,OrderService 通过初始化器接收 PayGateway,这就是 DI。也就是不在内部直接创建所需对象,而是从外部传入。

注入方式主要有三种。

注入方式 说明 推荐程度
构造器注入 通过 init 接收依赖 最推荐(不可变,必需依赖明确)
方法注入 通过方法参数接收 适用于可选依赖
属性注入 创建后赋值给属性 会变成可选且为 var,因此不推荐

Swift 推荐使用初始化器注入。依赖可以用 let 固定,更安全;测试时也很容易传入伪对象(Mock)。

总结起来,三者的关系如下。

为了遵守 DIP 原则,沿着 IoC 的方向移交控制权,并使用 DI 作为具体手段。

这句话最简洁地概括了三个术语之间的关系。

白板上用箭头连接 WHY、WHO、HOW 三个方框的总结图
按为什么、谁、如何三个问题分别记录,就不容易混淆

常见问题总结

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 相关代码会呈现出完全不同的样子。希望这篇文章能帮助你彻底理清这些概念。加油!

延伸阅读