依赖反转的定义
在面向对象开发中有一个基本的设计原则叫依赖反转/倒置(dependency inversion)。维基百科对于它的定义如下:
- 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
- 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
剖析
但是当我看到这样的定义时,我是十分困惑的。什么是依赖?什么是反转?为什么定义了抽象接口,作为中间层会被称为依赖反转?如果你和我有相同的疑问,不妨跟我一起看下去。
我们先看一下下面这段传统的示例代码片段 1:
class B {
void mb(){}
}
class A {
public void cb() {
(new B()).mb();
}
}
上面的代码非常简单,在 A
的 cb
方法创建了 B
的对象并调用了它的 mb
方法。 在这里 A
需要 B
才能实现 cb
的功能,所以我们称 A
依赖于 B
。 这里我们同时需要知道的是 A
是 “高层次”, “B” 属于低层次。
按照‘‘依赖反转’’的定义, 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口
, 所以 “B” 是要抽象出来的。我们把上面的代码改造成下面的代码片段 2:
interface IB {
void mb();
}
class B implements IB {
void mb(){}
}
class A {
public void cb(IB b) {
b.mb();
}
}
这样A
不再依赖于具体的 B
的对象,而是依赖于 IB
接口。B
也实现了 IB
接口。可以说这里我们已经就满足了 “依赖反转” 的定义:
A
不依赖于B
,A
和B
都依赖于抽象接口IB
。IB
不依赖于具体实现A
、B
。而具体实现B
、A
则应该依赖于抽象接口IB
。
总结下,现在依赖关系的变化:A -> B
变成了 A -> IB, B -> IB
问题来了,“反转” 从何而来?不妨继续看看下面的代码。
void main() {
(new A()).cb();
}
在这段代码中,我们在 main
函数,调用了 代码片段 1 中 A.cb
方法。不难理解,在这里,main
函数其实又是 A
的高层次。我们实现的是高层次依次调用低层次的具体实现, main -> new A() -> new B()。接着我们看看 main
调用 代码片段2 会有什么不同?
void main() {
IB b = new B();
(new A(b)).cb();
}
写到这里,我忽然觉得不需要解释什么了,代码已经说明了一切。对于高层次的 A
来讲,低层次的 B
却被先创建了出来,变成了 main -> new B() -> new A()。这就是 “反转” 的意思。低层次的实现被推迟到了更高的层次。
总结,依赖反转是从调用者的视角看的。传统的代码实现,是高层次调用低层次的具体实现,但是使用了依赖反转后,低层次的具体实现,被提升到了更高的层次。
扩展
依赖反转的优点:
上面我们结合概念和代码,解释了什么是依赖反转。那么依赖反转的优点是什么?
- 解耦合:
A
不再强绑定于B
, 任何实现了IB
接口的类,都能被传入A
中; - 扩展性:任何实现了
IB
接口的类,都能被传入到A
中,而无需修改A
的代码;
其他 “依赖” 的表现形式
class A {
C mc;
public A(C c) {
mc = c;
}
public setD(D d) {
....
}
}
在上面的构造函数, setD
都属于依赖。想必会加深对依赖的理解。
依赖反转和依赖注入是什么关系
依赖注入是依赖反转的一种具体的实现方式。
自问自答
自问: 如果没有创建 IB
接口,通过下面的方式实现,是不是也是依赖反转?
class A {
public void cb(B b) {
b.mb();
}
}
void main() {
B b = new B();
(new A(b)).cb();
}
自答: 首先毫无疑问这是依赖注入,依赖注入是依赖反转的一种具体实现,所以是。在 dart
语言中,类本身也是接口,从这种层面上来讲,完全是 ok 的。但是从 java 来讲又不完全是,因为不符合依赖反转的具体定义,只能传 B
的对象, 这就破坏了代码的灵活性、扩展性、非耦合性,就算它是,也是一种很烂的实现。
所以我们应该严格按照依赖反转的定义去编写我们具体的代码。