昨天學習了六個設計原則中的單一職責原則和里氏替換原則,今天繼續學習依賴倒置原則和接口隔離原則,因爲都是一些偏理論的東西,雖說理解,但在使用中還是會比較喫力,建議沒事的時候多回過頭來看幾遍,孰能生巧,用起來也會得心應手。
依賴倒置原則
依賴倒置原則(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-1所示。
奔馳車可以提供一個方法run,代表車輛運行,實現過程如代碼清單3-1所示。
public class Driver {//司機的主要職責就是駕駛汽車
public void drive(Benz benz){
benz.run();
}
}
司機通過調用奔馳車的run方法開動奔馳車,其源代碼如下所示。
public class Benz {//汽車肯定會跑
public void run(){
System.out.println("奔馳汽車開始運行...");
}
}
有車,有司機,在Client場景類產生相應的對象,其源代碼如下所示。
public class Client {
public static void main(String[] args) {
Driver zhangSan = new Driver();
Benz benz = new Benz();
zhangSan.drive(benz); //張三開奔馳車
}
}
通過以上的代碼,完成了司機開動奔馳車的場景,到目前爲止,這個司機開奔馳車的項目沒有任何問題。但是業務需求變更永無休止,技術前進就永無止境,在發生變更時才能發覺我們的設計或程序是否是松耦合。我們在一段貌似磐石的程序上加上一塊小石頭:張三司機不僅要開奔馳車,還要開寶馬車,又該怎麼實現呢?麻煩出來了,那好,我們走一步是一步,我們先把寶馬車產生出來。
public class BMW {//寶馬車當然也可以開動了
public void run(){
System.out.println("寶馬汽車開始運行...");
}
}
寶馬車也產生了,但是我們卻沒有辦法讓張三開動起來,爲什麼?張三沒有開動寶馬車的方法!這就是我們的設計出現了問題,司機類和奔馳車類之間是緊耦合的關係,其導致的結果就是系統的可維護性大大降低,可讀性降低,兩個相似的類需要閱讀兩個文件,你樂意嗎?還有穩定性,什麼是穩定性?固化的、健壯的纔是穩定的,這裏只是增加了一個車類就需要修改司機類,這不是穩定性,這是易變性。被依賴者的變更竟然讓依賴者來承擔修改的成本,這樣的依賴關係誰肯承擔!
設計是否具備穩定性,只要適當地“鬆鬆土”,觀察“設計的藍圖”是否還可以茁壯地成長就可以得出結論,穩定性較高的設計,在周圍環境頻繁變化的時候,依然可以做到“我自巋然不動”。
根據以上證明,如果不使用依賴倒置原則就會加重類間的耦合性,降低系統的穩定性,
增加並行開發引起的風險,降低代碼的可讀性和可維護性。承接上面的例子,引入依賴倒置
原則後的類圖如圖3-2所示。
建立兩個接口:IDriver和ICar,分別定義了司機和汽車的各個職能,司機就是駕駛汽
車,必須實現drive()方法,其實現過程如代碼所示。
public interface IDriver {
//是司機就應該會駕駛汽車
public void drive(ICar car);
}
public class Driver implements IDriver{
//司機的主要職責就是駕駛汽車
public void drive(ICar car){
car.run();
}
}
在IDriver中,通過傳入ICar接口實現了抽象之間的依賴關係,Driver實現類也傳入了ICar接口,至於到底是哪個型號的Car,需要在高層模塊中聲明。
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兩個實現類(細節),因此在高層次的模塊中應用都是抽象。
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類型進行操作,屏蔽了細節對抽象的影響。當然,張三如果要開寶馬車,也很容易,我們只要修改業務場景類就可以,
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。
對象的依賴關係有三種方式來傳遞,如下所示。
1. 構造函數傳遞依賴對象
在類中通過構造函數聲明依賴對象,按照依賴注入的說法,這種方式叫做構造函數注
入。
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();
}
}
2.Setter方法傳遞依賴對象
在抽象中設置Setter方法聲明依賴關係,依照依賴注入的說法,這是Setter依賴注入。
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.接口聲明依賴對象
在接口的方法中聲明依賴對象,上面奔馳,寶馬的例子就採用了接口聲明依賴的方式,該方法也叫做接口注入。
依賴倒置原則的本質就是通過抽象(接口或抽象類)使各個類或模塊的實現彼此獨立,不互相影響,實現模塊間的松耦合,我們怎麼在項目中使用這個規則呢?只要遵循以下的幾個規則就可以:
每個類儘量都有接口或抽象類,或者抽象類和接口兩者都具備
● 變量的表面類型儘量是接口或者是抽象類
● 任何類都不應該從具體類派生
● 儘量不要覆寫基類的方法
● 結合里氏替換原則使用
接口隔離原則
在講接口隔離原則之前,先明確一下我們的主角——接口。接口分爲兩種:
● 實例接口(Object Interface),在Java中聲明一個類,然後用new關鍵字產生一個實例,它是對一個類型的事物的描述,這是一種接口。比如你定義Person這個類,然後使用Person zhangSan=new Person()產生了一個實例,這個實例要遵從的標準就是Person這個類,Person類就是zhangSan的接口。疑惑?看不懂?不要緊,那是因爲讓Java語言浸染的時間太長了,只要知道從這個角度來看,Java中的類也是一種接口。
● 類接口(Class Interface),Java中經常使用的interface關鍵字定義的接口。
主角已經定義清楚了,那什麼是隔離呢?它有兩種定義,如下所示:
- Clients should not be forced to depend upon interfaces that they don’t use.(客戶端不應該依賴它不需要的接口。)
- The dependency of one class to another one should depend on the smallest possible interface.(類間的依賴關係應該建立在最小的接口上。)
我們把這兩個定義剖析一下,先說第一種定義:“客戶端不應該依賴它不需要的接口”,那依賴什麼?依賴它需要的接口,客戶端需要什麼接口就提供什麼接口,把不需要的接口剔除掉,那就需要對接口進行細化,保證其純潔性;再看第二種定義:“類間的依賴關係應該建立在最小的接口上”,它要求是最小的接口,也是要求接口細化,接口純潔,與第一個定義如出一轍,只是一個事物的兩種不同描述。
我們可以把這兩個定義概括爲一句話:建立單一接口,不要建立臃腫龐大的接口。再通俗一點講:接口儘量細化,同時接口中的方法儘量少。
這與單一職責原則不是相同的嗎?錯,接口隔離原則與單一職責的審視角度是不相同的,單一職
責要求的是類和接口職責單一,注重的是職責,這是業務邏輯上的劃分,而接口隔離原則要
求接口的方法儘量少。
接口隔離原則是對接口進行規範約束,其包含以下4層含義:
● 接口要儘量小
這是接口隔離原則的核心定義,不出現臃腫的接口(Fat Interface),但是“小”是有限度
的,首先就是不能違反單一職責原則,並且根據接口隔離原則拆分接口時,首先必須滿足單一職責原則。
● 接口要高內聚
高內聚就是提高接口、類、模塊的處理能力,減少對外的交互。
● 定製服務
一個系統或系統內的模塊之間必然會有耦合,有耦合就要有相互訪問的接口(並不一定就是Java中定義的Interface,也可能是一個類或單純的數據交換),我們設計時就需要爲各個訪問者(即客戶端)定製服務,什麼是定製服務?定製服務就是單獨爲一個個調用者供最合適的接口。減少接口中的無用方法。
● 接口設計是有限度的
接口的設計粒度越小,系統越靈活,這是不爭的事實。但是,靈活的同時也帶來了結構
的複雜化,開發難度增加,可維護性降低,這不是一個項目或產品所期望看到的,所以接口
設計一定要注意適度,這個“度”如何來判斷呢?根據經驗和常識判斷,沒有一個固化或可測
量的標準。
接口隔離原則是對接口的定義,同時也是對類的定義,接口和類儘量使用原子接口或原子類來組裝。但是,這個原子該怎麼劃分是設計模式中的一大難題,在實踐中可以根據以下幾個規則來衡量:
● 一個接口只服務於一個子模塊或業務邏輯;
● 通過業務邏輯壓縮接口中的public方法,接口時常去回顧,儘量讓接口達到“滿身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
● 已經被污染了的接口,儘量去修改,若變更的風險較大,則採用適配器模式進行轉化處理;
● 瞭解環境,拒絕盲從。每個項目或產品都有特定的環境因素,別看到大師是這樣做的你就照抄。千萬別,環境不同,接口拆分的標準就不同。深入瞭解業務邏輯,最好的接口設計就出自你的手中!