10

DIP vs IoC vs DI

 3 years ago
source link: https://lotabout.me/2018/Dependency-Inversion-Principle/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Table of Contents

你听过 SOLID 原则吗?了解 Spring 的控制反转(IoC)吗?知道依赖注入(Dependency Injection)和它们有什么区别吗?虽然形式多样,它们的内核却很简单。

TLDR;

IoC Principle and patterns

(图片来源: http://www.tutorialsteacher.com/ioc/introduction)

DI is about wiring, IoC is about direction, and DIP is about shape.

  • DIP 是一种思想,它认为上层代码不应该依赖下层的实现,而应该提供接口让下层实现
  • IoC 是一种思想,它认为代码本职之外的工作都应该由某个第三方(框架)完成
  • DI 是一种技术,将依赖通过“注入”的方式提供给需要的类,是 DIP 和 IoC 的具体实现

它们的目的都是解耦,使程序更好地模块化,也使各个模块更容易测试。

依赖反转原则(Dependency Inversion Principle)

它是 SOLID 原则中的“D”,根据 维基百科

在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是 指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层 次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被 颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

从上面的描述我们可以看它的目的是解耦,手段是 “高层次的模块不依赖于低层次的模块的实现细节”。具体有两个原则:

  1. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
  2. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

例如下面的代码中,Service 的实现依赖于 DAO 的具体实现。如图:

DIP-dao-normal.png

上图中, 高层的 XXXService 依赖于 DAO 的实现细节,如果 DAO 是对 SQL 数据库进行操作,那也就决定了 XXXService 也只适用于 SQL 数据库,之后如果添加了 NOSQL 数据库,想再复用 XXXService 的逻辑就十分困难了。这是耦合的带来的弊端之一。

有 Java 经验的同学肯定会觉得解决方案很简单啊,不要用 DAO 类啊,先实现一个 DAO 接口,再实现一个实现类不就搞定了嘛。这个习惯似乎已经成了一种铁律,但没错,DIP 的实践告诉我们 XXXService 应该创建一个 DAO 接口,而具体的实现类则负责实现这个接口,如下:

DIP-dao-dip.png

上面这张图有两点要注意的地方:

  1. DAO 接口是和 XXXService(调用方)在同一层而不是和 SQLDao (实现方)在一层
  2. XXXService 依赖同一层的接口,在下层的 SQLDao 实现了上一层的接口。

因此依赖反转, “反转”的是上下层的依赖,由上层依赖下层的实现,反转成下层依赖上层的接口

而在实现上也很容易理解:不要在一个类里显示 new 另一个类(当然一般来说这个类是 Service 或 Component,而不是普通的数据类)。

控制反转(Inversion of Control)

那么什么是控制反转呢?这篇文章 讲得很清晰:

IoC is a design principle which recommends inversion of different kinds of controls in object oriented design to achieve loose coupling between the application classes. Here, the control means any additional responsibilities a class has other than its main responsibility, such as control over the flow of an application, control over the dependent object creation and binding

IoC 是一个 设计原则,它提倡我们反转面向对象设计中的各种控制,以达到各个类之间的解耦。这里“控制”的含义是除了一个类本职之外的其它所有工作,如整个软件流程的控制,依赖或绑定的创建等。

IoC 的各种教程里,常说控制反转和“好莱坞原则”一致:“Don’t call me, I’ll call you.”

维基百科的例子 举得很好:

例如写传统的命令行程序,我们需要展示给用户一些菜单,然后根据用户的选择做相应的操作。于是我们写了一个菜单类,这个菜单类会调用底层的“显示类”来显示菜单内容,监听并返回用户的选择。

现在如果我们将代码移植到图形界面,对应地就要创建“GUI显示类”,此时需要我们修改菜单类来适应之种修改。我们只是要修改显示相关的内容却要却个性菜单类,这说明菜单类与显示类间有耦合。

控制反转认为菜单类的本职工作是提供“菜单”,如何显示,如何处理用户选择等不应该是它的职责(单一职责原则)。因此,最好有一个框架专门管理整体流程。框架知道有的类负责显示、有的类负责提供菜单,同时它也知道如何软件的流程,知道什么时候需要调用菜单类得到菜单,什么时候需要调用显示类做展示。框架定好流程,各个类“填空”就行了,菜单类提供菜单内容,显示类提供显示逻辑等等。

控制反转是把不属于类的职责抽离出来,让一个专门的“第三方”来做处理这些事。所以它的外延其实是很广的,我们常说的 IoC 容器只是一个专门的“第三方”用来处理依赖罢了。

依赖注入(Dependency Injection)

在实际使用和讨论中,大家滥用了 IoC 这个词,因此 Martin Fowler 等人在讨论后,确定使用“依赖注入”(DI)这个词来指代其中一项具体的技术。DI 技术背后的想法是:

  1. 为了保证 DIP 原则,一个类应该只依赖抽象接口。
  2. 于是具体的实现需要由某种方式“注入”到这个类。
  3. 那么依据控制反转的思想,最好是由第三方(容器)来完成。

“注入”又有几种方式:

  1. constructor injection ,依赖通过构造函数传入
  2. setter injection,依赖通过一个个 setter 传入
  3. interface injection,类显示实现一个 setter interface。

对实现细节感兴趣的话可以看 维基百科 的例子。

要明白的是依赖注入只是“注入”依赖的其中一种方式(使用最广吧),还有一些其它的方式,例如“依赖查找”(Dependency Lookup),这里就不深入了。

注意的是依赖注入是只明确 如何 将依赖“注入”一个类,而由谁来做并不是 DI 处理的问题,例如在 Python 等其它语言里,我们依旧可以贯彻 DIP,也可以用 constructor injection,但与 Java 中使用 IoC 容器来管理不同,Python 中大家很少使用甚至听说 IoC 容器。

现实中的应用

这部分是看了陈浩的 IOC/DIP其实是一种管理思想 后想到的。其实计算机中的许多概念在现实中也是有对应的,按我的理解:

  • DIP 相当于“标准化”产品
  • IoC 相当于“流水线”化环节

就比如说一家餐厅用的海鲜全是某个供应商供应的,后来由于店面扩大,想换一家更大供应商,但发现供应商能供应的种类和质量都和之前不同,因此换供应商的同时就要让改菜单,大厨们对一些食材要特殊处理。可见餐厅和之前的供应商耦合太高。

DIP 告诉我们,餐厅不应该直接依赖某个供应商,而应该规定供应商的标准。要成为自己的供应商,必须能提供什么种类的食材,食材要达到什么标准,这样即使想换供应商,餐厅自己也不需要有任何变化。这时餐厅不是依赖于具体的供应商,而是依赖于制定的供应商标准。

再比如还是这家餐厅,但它是连锁餐厅,尽管不同的供应商都满足了标准,但每家子餐厅还是自己挑选供应商,而现在总公司决定缩减成本,选择价格更低的供应商,由于每家子餐厅都是自己选择,要实施这个命令就很困难。

而 IoC 认为餐厅的职责应该是生产食品,而原料的供应、定单的接收乃至食物的配送都不应该是餐厅(或者应该称为厨房)负责的。于是总公司就专门成立一个管理部门,负责管理整个流程,它为每个步骤都创建一个具体的部分,统筹规划。采购部分负责选择供应商,管理部门把得到的原料和定单交给餐厅,餐厅只专注生产。相当于建立了一个流水线,每一个部分都成了流水线的一个步骤,都专注于自己的职责。另一方面,流水线的管理也专注于流程的管理。

最后还是想说所有的设计都是在做 trade-off。例如模块化能使软件更容易变化,模块之间能替换,但实际生产中,有多少软件会换自己的数据库呢?再比如说 IoC 其实也要看个度,如果所有的控制流都反转了,那管理起来也会过于复杂。

吐糟下似乎 Java 的开发者都特别喜欢上来先写个接口,然后写一个实现类。写起来不容易,读起来也费劲,但你要是问起来,大家会说这样有得写单元测试,并且如果需要替换实现类也会更方便。可是实际上,几乎 95% 以上(随便说的数字,实际中一次都没看见过)的类都不会有两个或以上的实现,而测试其实也可以通过生成子类之类的方式来做。

因此,我想说学习的时候还是要搞懂它要解决什么问题,有什么好处和缺点,这样才能具体问题具体分析。世上没有银弹。

Reference


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK