3.1 依賴倒置原則的定義
???? 依賴倒置原則(Dependence Inversion Principle,簡稱DIP)這個名字看著有點別扭,“依賴”還“倒置”,這到底是什么意思?依賴倒置原則的原始定義是:High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions。翻譯過來,包含三層含義:
- 高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;
- 抽象不應該依賴細節;
- 細節應該依賴抽象。
???? 高層模塊和低層模塊容易理解,每一個邏輯的實現都是由原子邏輯組成的,不可分割的原子邏輯就是低層模塊,原子邏輯的再組裝就是高層模塊。那什么是抽象,什么又是細節呢?在Java語言中,抽象就是指接口或抽象類,兩者都是不能直接被實例化的;細節就是實現類,實現接口或繼承抽象類而產生的類就是細節,其特點就是可以直接被實例化,也就是可以加上一個關鍵字new產生一個對象。依賴倒置原則在Java語言中的表現就是:
- 模塊間的依賴是通過抽象發生,實現類之間不發生直接的依賴關系,其依賴關系是通過接口或抽象類產生的;
- 接口或抽象類不依賴于實現類;
- 實現類依賴接口或抽象類。
???? 更加精簡的定義就是“面向接口編程”——OOD(Object-Oriented Design,面向對象設計)的精髓之一。
3.2 言而無信,你太需要契約
???? 采用依賴倒置原則可以減少類間的耦合性,提高系統的穩定性,減少并行開發引起的風險,提高代碼的可讀性和可維護性。
???? 證明一個定理是否正確,有兩種常用的方法:一種是根據提出的論題,經過一番論證,推出和定理相同的結論,這是順推證法;還有一種是首先假設提出的命題是偽命題,然后推導出一個荒謬、與已知條件互斥的結論,這是反證法。我們今天就用反證法來證明依賴倒置原則是多么的優秀和偉大!
???? 論題:依賴倒置原則可以減少類間的耦合性,提高系統的穩定性,減少并行開發引起的風險,提高代碼的可讀性和維護性。
???? 反論題:不使用依賴倒置原則也可以減少類間的耦合性,提高系統的穩定性,減少并行開發引起的風險,提高代碼的可讀性和維護性。
???? 我們通過一個例子來說明反論題是不成立的。現在的汽車越來越便宜了,也就頂多一個衛生間的價格就可以買到一輛不錯的汽車,有汽車就必然有人來駕駛了,司機駕駛奔馳車的類圖如圖3-1所示。
圖3-1 司機駕駛奔馳車類圖
???? 奔馳車可以提供一個方法run,代表車輛運行,實現過程如代碼清代3-1所示。
代碼清單3-1 司機源代碼
1
2
3
4
5
6
7
8
9
10
11
|
public
class
Driver {
?
?
//司機的主要職責就是駕駛汽車
?
?
public
void
drive(Benz benz){
?
?
benz.run();
?
?
}
?
?
}
|
???? 司機通過調用奔馳車的run方法開動奔馳車,其源代碼如代碼清單3-2所示。
代碼清單3-2 奔馳車源代碼
1
2
3
4
5
6
7
8
9
10
11
|
public
class
Benz {
?
?
//汽車肯定會跑
?
?
public
void
run(){
?
?
System.out.println(
"奔馳汽車開始運行..."
);
?
?
}
?
?
}
|
???? 有車,有司機,在Client場景類產生相應的對象,其源代碼如代碼清代3-3所示。
代碼清單3-3 場景類源代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public
class
Client {
?
?
public
static
void
main(String[] args) {
?
?
Driver zhangSan =
new
Driver();
?
?
Benz benz =
new
Benz();
?
?
//張三開奔馳車
?
?
zhangSan.drive(benz);
?
?
}
?
?
}
|
???? 通過以上的代碼,完成了司機開動奔馳車的場景,到目前為止,這個司機開奔馳車的項目沒有任何問題。我們常說“危難時刻見真情”,我們把這句話移植到技術上就成了“變更才顯真功夫”,業務需求變更永無休止,技術前進就永無止境,在發生變更時才能發覺我們的設計或程序是否是松耦合。我們在一段貌似磐石的程序上加上一塊小石頭:張三司機不僅要開奔馳車,還要開寶馬車,又該怎么實現呢?麻煩出來了,那好,我們走一步是一步,我們先把寶馬車產生出來,實現過程如代碼清單3-4所示。
代碼清單3-4 寶馬車源代碼
1
2
3
4
5
6
7
8
9
10
11
|
public
class
BMW {
?
?
//寶馬車當然也可以開動了
?
?
public
void
run(){
?
?
System.out.println(
"寶馬汽車開始運行..."
);
?
?
}
?
?
}
|
???? 寶馬車也產生了,但是我們卻沒有辦法讓張三開動起來,為什么?張三沒有開動寶馬車的方法呀!一個拿有C照的司機竟然只能開奔馳車而不能開寶馬車,這也太不合理了!在現實世界都不允許存在這種情況,何況程序還是對現實世界的抽象,我們的設計出現了問題:司機類和奔馳車類之間是一個緊耦合的關系,其導致的結果就是系統的可維護性大大降低,可讀性降低,兩個相似的類需要閱讀兩個文件,你樂意嗎?還有穩定性,什么是穩定性?固化的、健壯的才是穩定的,這里只是增加了一個車類就需要修改司機類,這不是穩定性,這是易變性。被依賴者的變更竟然讓依賴者來承擔修改的成本,這樣的依賴關系誰肯承擔!證明到這里,我們已經知道偽命題已經部分不成立了。
???? 注意 設計是否具備穩定性,只要適當的“松松土”,觀察“設計的藍圖”是否還可以茁壯的成長就可以得出結論,穩定性較高的設計,在周圍環境頻繁變化的時候,依然可以做到“我自巋然不動”。
???? 我們繼續證明,“減少并行開發引起的風險”,什么是并行開發的風險?并行開發最大的風險就是風險擴散,本來只是一段程序的錯誤或異常,逐步波及一個功能,一個模塊,甚至到最后毀壞了整個項目,為什么并行開發就有這個風險呢?一個團隊,20人開發,各人負責不同的功能模塊,甲負責汽車類的建造,乙負責司機類的建造,在甲沒有完成的情況下,乙是不能完全地編寫代碼的,缺少汽車類,編譯器根本就不會讓你通過!在缺少Benz類的情況下,Driver類能編譯嗎?更不要說是單元測試了!在這種不使用依賴倒置原則的環境中,所有的開發工作都是“單線程”的,甲做完,乙再做,然后是丙繼續…,這在90年代“個人英雄主義”編程模式中還是比較適用的,一個人完成所有的代碼工作,但在現在的大中型項目中已經是完全不能勝任了,一個項目是一個團隊的協作結果,一個“英雄”再牛X也不可能了解所有的業務和所有的技術,要協作就要并行開發,要并行開發就要解決模塊之間的項目依賴關系,那然后呢?依賴倒置原則就隆重出場了!
???? 根據以上證明,如果不使用依賴倒置原則就會加重類間的耦合性,降低系統的穩定性,增加并行開發引起的風險,降低代碼的可讀性和維護性。承接上面的例子,引入依賴倒置原則后的類圖如圖3-2所示。
圖3-2 引入依賴倒置原則后的類圖
???? 建立兩個接口:IDriver和ICar,分別定義了司機和汽車的各個職能,司機就是駕駛汽車,必須實現drive()方法,其實現過程如代碼清單3-5所示。
代碼清單3-5 司機接口
1
2
3
4
5
6
7
|
public
interface
IDriver {
?
?
//是司機就應該會駕駛汽車
?
?
public
void
drive(ICar car);
?
?
}
|
???? 接口只是一個抽象化的概念,是對一類事物的最抽象描述,具體的實現代碼由相應的實現類來完成,Driver實現類如代碼清單3-6所示。
代碼清單3-6 司機類的實現
1
2
3
4
5
6
7
8
9
10
11
|
public
class
Driver
implements
IDriver{
?
?
//司機的主要職責就是駕駛汽車
?
?
public
void
drive(ICar car){
?
?
car.run();
?
?
}
?
?
}
|
???? 在IDriver中,通過傳入ICar接口實現了抽象之間的依賴關系,Driver實現類也傳入了ICar接口,至于到底是哪個型號的Car需要在高層模塊中聲明。
???? ICar及其兩個實現類的實現過程如代碼清單3-7所示。
代碼清單3-7 汽車接口及兩個實現類
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public
interface
ICar {
?
?
//是汽車就應該能跑
?
?
public
void
run();
?
?
}
?
?
public
class
Benz
implements
ICar{
?
?
//汽車肯定會跑
?
?
public
void
run(){
?
?
System.out.println(
"奔馳汽車開始運行..."
);
?
?
}
?
?
}
?
?
public
class
BMW
implements
ICar{
?
?
//寶馬車當然也可以開動了
?
?
public
void
run(){
?
?
System.out.println(
"寶馬汽車開始運行..."
);
?
?
}
?
?
}
|
???? 在業務場景中,我們貫徹“抽象不應該依賴細節”,也就是我們認為抽象(ICar接口)不依賴BMW和Benz兩個實現類(細節),因此我們在高層次的模塊中應用都是抽象,Client的實現過程如代碼清單3-8所示。
代碼清單3-8 業務場景
public class Client {
public static void main(String[] args) {
IDriver zhangSan = new Driver();
ICar benz = new Benz();
//張三開奔馳車
zhangSan.drive(benz);
}
}
???? Client屬于高層業務邏輯,它對低層模塊的依賴都建立在抽象上,zhangSan的顯示類型是IDriver,benz的顯示類型是ICar,也許你要問,在這個高層模塊中也調用到了低層模塊,比如new Driver()和new Benz()等,如何解釋?確實如此,zhangSan的顯示類型是IDriver,是一個接口,是抽象的,非實體化的,在其后的所有操作中,zhangSan都是以IDriver類型進行操作,屏蔽了細節對抽象的影響。當然,張三如果要開寶馬車,也很容易,我們只要修改業務場景類就可以,實現過程如代碼清單3-9所示。
代碼清單3-9 張三駕駛寶馬車的實現過程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public
class
Client {
?
?
public
static
void
main(String[] args) {
?
?
IDriver zhangSan =
new
Driver();
?
?
ICar bmw =
new
BMW();
?
?
//張三開奔馳車
?
?
zhangSan.drive(bmw);
?
?
}
?
?
}
|
???? 在新增加低層模塊時,只修改了業務場景類,也就是高層模塊,對其他低層模塊如Driver類不需要做任何修改,業務就可以運行,把“變更”引起的風險擴散降低到最小。
???? 注意 在Java中,只要定義變量就必然要有類型,一個變量可以有兩個類型:顯示類型和真實類型,顯示類型是在定義的時候賦予的類型,真實類型是對象的類型,如zhangSan的顯示類型是IDriver,真實類型是Driver。
???? 我們再來思考依賴倒轉對并行開發的影響。兩個類之間有依賴關系,只要制定出兩者之間的接口(或抽象類)就可以獨立開發了,而且項目之間的單元測試也可以獨立的運行,而TDD(Test-Driven Development,測試驅動開發)開發模式就是依賴倒置原則的最高級應用。我們繼續回顧上面司機駕駛汽車的例子,甲程序員負責IDriver的開發,乙程序員負責ICar的開發,兩個開發人員只要制定好了接口就可以獨立地開發了,甲開發進度比較快,完成了IDriver以及相關的實現類Driver的開發工作,而乙程序員滯后開發,那甲是否可以進行單元測試(Unit Test)呢?答案是可以,我們引入一個JMock工具,其最基本的功能是根據抽象虛擬一個對象進行測試,測試類如代碼清單3-10所示。
代碼清單3-10 測試類
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public
class
DriverTest
extends
TestCase{
?
?
Mockery context =
new
JUnit4Mockery();
?
?
@Test
?
?
public
void
testDriver() {
?
?
//根據接口虛擬一個對象
?
?
final
ICar car = context.mock(ICar.
class
);
?
?
IDriver driver =
new
Driver();
?
?
//內部類
?
?
context.checking(
new
Expectations(){{
?
?
oneOf (car).run();
?
?
}});
?
?
driver.drive(car);
?
?
}
?
?
}
|
???? 注意粗體部分,我們只需要一個ICar的接口,就可以對Driver類進行單元測試,從這一點來看,兩個相互依賴的對象可以分別進行開發,孤立的單元測試,進而保證并行開發的效率和質量,TDD開發的精髓不就在這里嗎?測試驅動開發,先寫好單元測試類,然后再寫實現類,這對代碼的質量有非常大的提高,特別適合研發類項目或在項目成員整體水平比較低的情況下采用。
???? 抽象是對實現的約束,對依賴者而言,也是一種契約,不僅僅約束自己,還同時約束自己與外部的關系,其目的是保證所有的細節不逃脫契約的范疇,確保約束雙方按照既定的契約(抽象)共同發展,只要抽象這根基線在,細節就逃脫不了這個圈圈,始終讓你的對象做到“言而有信”“言出必行”
3.3 依賴的三種寫法
???? 依賴是可以傳遞的,A對象依賴B對象,B又依賴C,C又依賴D…,生生不息,依賴不止,記住一點:只要做到抽象依賴,即使是多層的依賴傳遞也無所畏懼!
???? 對象的依賴關系有三種方式來傳遞,如下所示。
- 構造函數傳遞依賴對象
???? 在類中通過構造函數聲明依賴對象,按照依賴注入的說法這種方式叫做構造函數注入,按照這種方式的注入,IDriver和Driver的程序修改后如代碼清單3-11所示。
代碼清單3-11 構造函數傳遞依賴對象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public
interface
IDriver {
?
?
//是司機就應該會駕駛汽車
?
?
public
void
drive();
?
?
}
?
?
public
class
Driver
implements
IDriver{
?
?
private
ICar car;
?
?
//構造函數注入
?
?
public
Driver(ICar _car){
?
?
this
.car = _car;
?
?
}
?
?
//司機的主要職責就是駕駛汽車
?
?
public
void
drive(){
?
?
this
.car.run();
?
?
}
?
?
}
|
- Setter方法傳遞依賴對象
???? 在抽象中設置setter方法聲明依賴關系,依照依賴注入的說法就是setter依賴注入,按照這種方式的注入,IDriver和Driver的程序修改后如代碼清單3-12所示。
代碼清單3-12 Setter依賴注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public
interface
IDriver {
?
?
//車輛型號
?
?
public
void
setCar(ICar car);
?
?
//是司機就應該會駕駛汽車
?
?
public
void
drive();
?
?
}
?
?
public
class
Driver
implements
IDriver{
?
?
private
ICar car;
?
?
public
void
setCar(ICar car){
?
?
this
.car = car;
?
?
}
?
?
//司機的主要職責就是駕駛汽車
?
?
public
void
drive(){
?
?
this
.car.run();
?
?
}
?
?
}
|
- 接口聲明依賴對象
???? 在接口的方法中聲明依賴對象,3.2章節的例子就是采用了接口聲明依賴的方式,,該方法也叫做接口注入。
3.4 最佳實踐
???? 依賴倒轉原則的本質就是通過抽象(接口或抽象類)使各個類或模塊的實現彼此獨立,不互相影響,實現模塊間的松耦合,我們怎么在項目中使用這個規則呢?只要遵循以下的幾個規則就可以:
- 每個類盡量都有接口或抽象類,或者抽象類和接口兩者都具備。
???? 這是依賴倒置的基本要求,接口和抽象類都是屬于抽象的,有了抽象才可能依賴倒置。
- 變量的顯示類型盡量是接口或者是抽象類。
???? 很多書上說變量的類型一定要是接口或者是抽象類,這個有點絕對化了,比如一個工具類,xxxUtils一般是不需要接口或是抽象類的。還有,如果你要使用類的clone方法,就必須使用實現類,這個是JDK提供一個規范。
- 任何類都不應該從具體類派生。
???? 如果一個項目處于開發狀態,確實不應該有從具體類派生出的子類的情況,但這也不是絕對的,因為人都是會犯錯誤的,有時設計缺陷是在所難免的,因此只要不超過兩層的繼承都是可以忍受的。特別是做項目維護的同志,基本上可以不考慮這個規則,為什么?維護工作基本上都是做擴展開發,修復行為,通過一個繼承關系,覆寫一個方法就可以修正一個很大的Bug,何必再要去繼承最高的基類呢?
- 盡量不要覆寫基類的方法。
???? 如果基類是一個抽象類,而且這個方法已經實現了,子類盡量不要覆寫。類間依賴的是抽象,覆寫了抽象方法,對依賴的穩定性會產生一定的影響。
- 結合里氏替換原則使用
???? 在上一個章節中我們講解了里氏替換原則,父類出現的地方子類就能出現,再結合本章節的講解,我們可以得出這樣一個通俗的規則: 接口負責定義public屬性和方法,并且聲明與其他對象的依賴關系,抽象類負責公共構造部分的實現,實現類準確的實現業務邏輯,同時在適當的時候對父類進行細化。
???? 講了這么多,估計大家對“倒置”這個詞還是有點不理解,那到底什么是“倒置”呢?我們先說“正置”是什么意思,依賴正置就是類間的依賴是實實在在的實現類間的依賴,也就是面向實現編程,這也是正常人的思維方式,我要開奔馳車就依賴奔馳車,我要使用筆記本電腦就直接依賴筆記本電腦,而編寫程序需要的是對現實世界的事物進行抽象,抽象的結果就是有了抽象類和接口,然后我們根據系統設計的需要產生了抽象間的依賴,代替了人們傳統思維中的事物間的依賴,“倒置”就是從這里產生的。
???? 依賴倒置原則的優點在小型項目中很難體現出來,例如小于10個人月的項目,使用簡單的SSH架構,基本上不費太大力氣就可以完成,是否采用依賴倒置原則影響不大。但是,在一個大中型項目中,采用依賴倒置原則可以帶來非常多的優點,特別是規避一些非技術因素引起的問題。項目越大,需求變化的概率也越大,通過采用依賴倒置原則設計的接口或抽象類對實現類進行約束,可以減少需求變化引起的工作量劇增的情況。人員的變動在大中型項目中也是時常存在的,如果設計優良、代碼結構清晰,人員變化對項目的影響基本為零。大中型項目的維護周期一般都很長,采用依賴倒置原則可以讓維護人員輕松地擴展和維護。
???? 依賴倒置原則是六個設計原則中最難以實現的原則,它是實現開閉原則的重要途徑,依賴倒置原則沒有實現,就別想實現對擴展開放,對修改關閉。在項目中,大家只要記住是“面向接口編程”就基本上抓住了依賴倒轉原則的核心。
???? 講了這么多依賴倒置原則的優點,我們也來打擊一下大家,在現實世界中確實存在著必須依賴細節的事物,比如法律,就必須依賴細節的定義。 “殺人償命”在中國的法律中古今有之,那這里的殺人就是一個抽象的含義,怎么殺,殺什么人,為什么殺人,都沒有定義,只要是殺人就統統得償命,那這就是有問題了,好人殺了壞人,還要陪上自己的一條性命,這是不公正的,從這一點看,我們在實際的項目中使用依賴倒置原則時需要審時度勢,不要抓住一個原則不放,每一個原則的優點都是有限度的,并不是放之四海而皆準的真理,所以別為了遵循一個原則而放棄了一個項目的終極目標:投產上線和盈利。作為一個項目經理或架構師,應該懂得技術只是實現目的的工具,惹惱了頂頭上司,設計做得再漂亮,代碼寫得再完美,項目做得再符合標準,一旦項目虧本,產品投入大于產出,那整體就是扯淡!你自己也別想混得更好!
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
