依賴反轉的定義
在面向對象開發中有一個基本的設計原則叫依賴反轉/倒置(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
的對象, 這就破壞了代碼的靈活性、擴展性、非耦合性,就算它是,也是一種很爛的實現。
所以我們應該嚴格按照依賴反轉的定義去編寫我們具體的代碼。