3.2 言而無信,你太需要契約
采用依賴倒置原則可以減少類間的耦合性,提高系統的穩定性,降低并行開發引起的風險,提高代碼的可讀性和可維護性。
證明一個定理是否正確,有兩種常用的方法:一種是根據提出的論題,經過一番論證,推出和定理相同的結論,這是順推證法;還有一種是首先假設提出的命題是偽命題,然后推導出一個荒謬、與已知條件互斥的結論,這是反證法。我們今天就用反證法來證明依賴倒置原則是多么優秀和偉大!
論題:依賴倒置原則可以減少類間的耦合性,提高系統的穩定性,降低并行開發引起的風險,提高代碼的可讀性和可維護性。
反論題:不使用依賴倒置原則也可以減少類間的耦合性,提高系統的穩定性,降低并行開發引起的風險,提高代碼的可讀性和可維護性。
我們通過一個例子來說明反論題是不成立的。現在的汽車越來越便宜了,一個衛生間的造價就可以買到一輛不錯的汽車,有汽車就必然有人來駕駛,司機駕駛奔馳車的類圖如圖3-1所示。

圖3-1 司機駕駛奔馳車類圖
奔馳車可以提供一個方法run,代表車輛運行,實現過程如代碼清單3-1所示。
代碼清單3-1 司機源代碼
public?class?Driver?{??????
?????//司機的主要職責就是駕駛汽車
?????public?void?drive(Benz?benz){
?????????????benz.run();
?????}
}
司機通過調用奔馳車的run方法開動奔馳車,其源代碼如代碼清單3-2所示。
代碼清單3-2 奔馳車源代碼
public?class?Benz?{
?????//汽車肯定會跑
?????public?void?run(){
?????????????System.out.println("奔馳汽車開始運行...");
?????}
}
有車,有司機,在Client場景類產生相應的對象,其源代碼如代碼清單3-3所示。
代碼清單3-3 場景類源代碼
public?class?Client?{
?????public?static?void?main(String[]?args)?{
?????????????Driver?zhangSan?=?new?Driver();
?????????????Benz?benz?=?new?Benz();
?????????????//張三開奔馳車
?????????????zhangSan.drive(benz);
?????}
}
通過以上的代碼,完成了司機開動奔馳車的場景,到目前為止,這個司機開奔馳車的項目沒有任何問題。我們常說“危難時刻見真情”,我們把這句話移植到技術上就成了“變更才顯真功夫”,業務需求變更永無休止,技術前進就永無止境,在發生變更時才能發覺我們的設計或程序是否是松耦合。我們在一段貌似磐石的程序上加上一塊小石頭:張三司機不僅要開奔馳車,還要開寶馬車,又該怎么實現呢?麻煩出來了,那好,我們走一步是一步,我們先把寶馬車產生出來,實現過程如代碼清單3-4所示。
代碼清單3-4 寶馬車源代碼
public?class?BMW?{
?????//寶馬車當然也可以開動了
?????public?void?run(){
?????????????System.out.println("寶馬汽車開始運行...");
?????}
}
寶馬車也產生了,但是我們卻沒有辦法讓張三開動起來,為什么?張三沒有開動寶馬車的方法呀!一個拿有C駕照的司機竟然只能開奔馳車而不能開寶馬車,這也太不合理了!在現實世界都不允許存在這種情況,何況程序還是對現實世界的抽象,我們的設計出現了問題:司機類和奔馳車類之間是緊耦合的關系,其導致的結果就是系統的可維護性大大降低,可讀性降低,兩個相似的類需要閱讀兩個文件,你樂意嗎?還有穩定性,什么是穩定性?固化的、健壯的才是穩定的,這里只是增加了一個車類就需要修改司機類,這不是穩定性,這是易變性。被依賴者的變更竟然讓依賴者來承擔修改的成本,這樣的依賴關系誰肯承擔!證明到這里,我們已經知道反論題已經部分不成立了。
注意 設計是否具備穩定性,只要適當地“松松土”,觀察“設計的藍圖”是否還可以茁壯地成長就可以得出結論,穩定性較高的設計,在周圍環境頻繁變化的時候,依然可以做到“我自巋然不動”。
我們繼續證明,“減少并行開發引起的風險”,什么是并行開發的風險?并行開發最大的風險就是風險擴散,本來只是一段程序的錯誤或異常,逐步波及一個功能,一個模塊,甚至到最后毀壞了整個項目。為什么并行開發就有這樣的風險呢?一個團隊,20個開發人員,各人負責不同的功能模塊,甲負責汽車類的建造,乙負責司機類的建造,在甲沒有完成的情況下,乙是不能完全地編寫代碼的,缺少汽車類,編譯器根本就不會讓你通過!在缺少Benz類的情況下,Driver類能編譯嗎?更不要說是單元測試了!在這種不使用依賴倒置原則的環境中,所有的開發工作都是“單線程”的,甲做完,乙再做,然后是丙繼續……這在20世紀90年代“個人英雄主義”編程模式中還是比較適用的,一個人完成所有的代碼工作。但在現在的大中型項目中已經是完全不能勝任了,一個項目是一個團隊協作的結果,一個“英雄”再牛也不可能了解所有的業務和所有的技術,要協作就要并行開發,要并行開發就要解決模塊之間的項目依賴關系,那然后呢?依賴倒置原則就隆重出場了!
根據以上證明,如果不使用依賴倒置原則就會加重類間的耦合性,降低系統的穩定性,增加并行開發引起的風險,降低代碼的可讀性和可維護性。承接上面的例子,引入依賴倒置原則后的類圖如圖3-2所示。

圖3-2 引入依賴倒置原則后的類圖
建立兩個接口:IDriver和ICar,分別定義了司機和汽車的各個職能,司機就是駕駛汽車,必須實現drive()方法,其實現過程如代碼清單3-5所示。
代碼清單3-5 司機接口
public?interface?IDriver?{
?????//是司機就應該會駕駛汽車
?????public?void?drive(ICar?car);
}
接口只是一個抽象化的概念,是對一類事物的最抽象描述,具體的實現代碼由相應的實現類來完成,Driver實現類如代碼清單3-6所示。
代碼清單3-6 司機類的實現
public?class?Driver?implements?IDriver{????
?????//司機的主要職責就是駕駛汽車
?????public?void?drive(ICar?car){
?????????????car.run();
?????}
}
在IDriver中,通過傳入ICar接口實現了抽象之間的依賴關系,Driver實現類也傳入了ICar接口,至于到底是哪個型號的Car,需要在高層模塊中聲明。
ICar及其兩個實現類的實現過程如代碼清單3-7所示。
代碼清單3-7 汽車接口及兩個實現類
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 張三駕駛寶馬車的實現過程
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的開發工作,而乙程序員滯后開發,那甲是否可以進行單元測試呢?答案是可以,我們引入一個JMock工具,其最基本的功能是根據抽象虛擬一個對象進行測試,測試類如代碼清單3-10所示。
代碼清單3-10 測試類
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開發的精髓不就在這里嗎?測試驅動開發,先寫好單元測試類,然后再寫實現類,這對提高代碼的質量有非常大的幫助,特別適合研發類項目或在項目成員整體水平比較低的情況下采用。
抽象是對實現的約束,對依賴者而言,也是一種契約,不僅僅約束自己,還同時約束自己與外部的關系,其目的是保證所有的細節不脫離契約的范疇,確保約束雙方按照既定的契約(抽象)共同發展,只要抽象這根基線在,細節就脫離不了這個圈圈,始終讓你的對象做到“言必信,行必果”。
- 前言
- 第一部分 大旗不揮,誰敢沖鋒——6大設計原則全新解讀
- 第1章 單一職責原則
- 1.2 絕殺技,打破你的傳統思維
- 1.3 我單純,所以我快樂
- 1.4 最佳實踐
- 第2章 里氏替換原則
- 2.2 糾紛不斷,規則壓制
- 2.3 最佳實踐
- 第3章 依賴倒置原則
- 3.2 言而無信,你太需要契約
- 3.3 依賴的三種寫法
- 3.4 最佳實踐
- 第4章 接口隔離原則
- 4.2 美女何其多,觀點各不同
- 4.3 保證接口的純潔性
- 4.4 最佳實踐
- 第5章 迪米特法則
- 5.2 我的知識你知道得越少越好
- 5.3 最佳實踐
- 第6章 開閉原則
- 6.2 開閉原則的廬山真面目
- 6.3 為什么要采用開閉原則
- 6.4 如何使用開閉原則
- 6.5 最佳實踐
- 第二部分 真刀實槍 ——23種設計模式完美演繹
- 第7章 單例模式
- 7.2 單例模式的定義
- 7.3 單例模式的應用
- 7.4 單例模式的擴展
- 7.5 最佳實踐
- 第8章 工廠方法模式
- 8.2 工廠方法模式的定義
- 8.3 工廠方法模式的應用
- 8.4 工廠方法模式的擴展
- 8.5 最佳實踐
- 第9章 抽象工廠模式
- 9.2 抽象工廠模式的定義
- 9.3 抽象工廠模式的應用
- 9.4 最佳實踐
- 第10章 模板方法模式
- 10.2 模板方法模式的定義
- 10.3 模板方法模式的應用
- 10.4 模板方法模式的擴展
- 10.5 最佳實踐
- 第11章 建造者模式
- 11.2 建造者模式的定義
- 11.3 建造者模式的應用
- 11.4 建造者模式的擴展
- 11.5 最佳實踐
- 第12章 代理模式
- 12.2 代理模式的定義
- 12.3 代理模式的應用
- 12.4 代理模式的擴展
- 12.5 最佳實踐
- 第13章 原型模式
- 13.2 原型模式的定義
- 13.3 原型模式的應用
- 13.4 原型模式的注意事項
- 13.5 最佳實踐
- 第14章 中介者模式
- 14.2 中介者模式的定義
- 14.3 中介者模式的應用
- 14.4 中介者模式的實際應用
- 14.5 最佳實踐
- 第15章 命令模式
- 15.2 命令模式的定義
- 15.3 命令模式的應用
- 15.4 命令模式的擴展
- 15.5 最佳實踐
- 第16章 責任鏈模式
- 16.2 責任鏈模式的定義
- 16.3 責任鏈模式的應用
- 16.4 最佳實踐
- 第17章 裝飾模式
- 17.2 裝飾模式的定義
- 17.3 裝飾模式應用
- 17.4 最佳實踐
- 第18章 策略模式
- 18.2 策略模式的定義
- 18.3 策略模式的應用
- 18.4 策略模式的擴展
- 18.5 最佳實踐
- 第19章 適配器模式
- 19.2 適配器模式的定義
- 19.3 適配器模式的應用
- 19.4 適配器模式的擴展
- 19.5 最佳實踐
- 第20章 迭代器模式
- 20.2 迭代器模式的定義
- 20.3 迭代器模式的應用
- 20.4 最佳實踐
- 第21章 組合模式
- 21.2 組合模式的定義
- 21.3 組合模式的應用
- 21.4 組合模式的擴展
- 21.5 最佳實踐
- 第22章 觀察者模式
- 22.2 觀察者模式的定義
- 22.3 觀察者模式的應用
- 22.4 觀察者模式的擴展
- 22.5 最佳實踐
- 第23章 門面模式
- 23.2 門面模式的定義
- 23.3 門面模式的應用
- 23.4 門面模式的注意事項
- 23.5 最佳實踐
- 第24章 備忘錄模式
- 24.2 備忘錄模式的定義
- 24.3 備忘錄模式的應用
- 24.4 備忘錄模式的擴展
- 24.5 最佳實踐
- 第25章 訪問者模式
- 25.2 訪問者模式的定義
- 25.3 訪問者模式的應用
- 25.4 訪問者模式的擴展
- 25.5 最佳實踐
- 第26章 狀態模式
- 26.2 狀態模式的定義
- 26.3 狀態模式的應用
- 第27章 解釋器模式
- 27.2 解釋器模式的定義
- 27.3 解釋器模式的應用
- 27.4 最佳實踐
- 第28章 享元模式
- 28.2 享元模式的定義
- 28.3 享元模式的應用
- 28.4 享元模式的擴展
- 28.5 最佳實踐
- 第29章 橋梁模式
- 29.2 橋梁模式的定義
- 29.3 橋梁模式的應用
- 29.4 最佳實踐
- 第三部分 誰的地盤誰做主 ——設計模式PK
- 第30章 創建類模式大PK
- 30.1 工廠方法模式VS建造者模式
- 30.2 抽象工廠模式VS建造者模式
- 第31章 結構類模式大PK
- 31.1 代理模式VS裝飾模式
- 31.2 裝飾模式VS適配器模式
- 第32章 行為類模式大PK
- 32.1 命令模式VS策略模式
- 32.2 策略模式VS狀態模式
- 32.3 觀察者模式VS責任鏈模式
- 第33章 跨戰區PK
- 33.1 策略模式VS橋梁模式
- 33.2 門面模式VS中介者模式
- 33.3 包裝模式群PK
- 第四部分 完美世界 ——設計模式混編
- 第34章 命令模式+責任鏈模式
- 34.2 混編小結
- 第35章 工廠方法模式+策略模式
- 35.2 混編小結
- 第36章 觀察者模式+中介者模式
- 36.2 混編小結
- 第五部分 擴展篇
- 第37章 MVC框架
- 37.2 最佳實踐
- 第38章 新模式
- 38.1 規格模式
- 38.2 對象池模式
- 38.3 雇工模式
- 38.4 黑板模式
- 38.5 空對象模式
- 附錄 23種設計模式彩圖