1. 程式人生 > >依賴倒轉原則(Dependence Inversion Principle、DIP)

依賴倒轉原則(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,面向物件設計)的精髓之一。

 

二、例子

引入依賴倒置原則後的類圖如圖

 建立兩個介面:IDriver和ICar,分別定義了司機和汽車的各個職能,司機就是駕駛汽車,必須實現drive()方法。

司機介面:

public interface IDriver {
//是司機就應該會駕駛汽車
public void drive(ICar car);
}

介面只是一個抽象化的概念,是對一類事物的最抽象描述,具體的實現程式碼由相應的實現類來完成,Driver實現類。

司機類的實現:

public class Driver implements IDriver{
    //司機的主要職責就是駕駛汽車
    public void drive(ICar car){
    car.run();
    } 
}

在IDriver中,通過傳入ICar介面實現了抽象之間的依賴關係,Driver實現類也傳入了ICar介面,至於到底是哪個型號的Car需要在高層模組中宣告。

     ICar及其兩個實現類的實現過程如下。

汽車介面及兩個實現類:

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的實現過程如下

業務場景:

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。

抽象是對實現的約束,對依賴者而言,也是一種契約,不僅僅約束自己,還同時約束自己與外部的關係,其目的是保證所有的細節不逃脫契約的範疇,確保約束雙方按照既定的契約(抽象)共同發展,只要抽象這根基線在,細節就逃脫不了這個圈圈,始終讓你的物件做到“言而有信”“言出必行”。

 

三、依賴的三種寫法

在實現依賴倒轉原則時,我們需要針對抽象層程式設計,而將具體類的物件通過依賴注入(DependencyInjection, DI)的方式注入到其他物件中,依賴注入是指當一個物件要與其他物件發生依賴關係時,通過抽象來注入所依賴的物件。常用的注入方式有三種,分別是:構造注入,設值注入(Setter注入)和介面注入。構造注入是指通過建構函式來傳入具體類的物件,設值注入是指通過Setter方法來傳入具體類的物件,而介面注入是指通過在介面中宣告的業務方法來傳入具體類的物件。這些方法在定義時使用的是抽象型別,在執行時再傳入具體型別的物件,由子類物件來覆蓋父類物件。

開閉原則、里氏代換原則和依賴倒轉原則,在大多數情況下,這三個設計原則會同時出現,開閉原則是目標,里氏代換原則是基礎,依賴倒轉原則是手段,它們相輔相成,相互補充,目標一致,只是分析問題時所站角度不同而已。

 

四、最佳實踐

 

依賴倒轉原則的本質就是通過抽象(介面或抽象類)使各個類或模組的實現彼此獨立,不互相影響,實現模組間的鬆耦合,我們怎麼在專案中使用這個規則呢?只要遵循以下的幾個規則就可以:

  • 每個類儘量都有介面或抽象類,或者抽象類和介面兩者都具備。

     這是依賴倒置的基本要求,介面和抽象類都是屬於抽象的,有了抽象才可能依賴倒置。

  • 變數的顯示型別儘量是介面或者是抽象類。

     很多書上說變數的型別一定要是介面或者是抽象類,這個有點絕對化了,比如一個工具類,xxxUtils一般是不需要介面或是抽象類的。還有,如果你要使用類的clone方法,就必須使用實現類,這個是JDK提供一個規範。

  • 任何類都不應該從具體類派生。

     如果一個專案處於開發狀態,確實不應該有從具體類派生出的子類的情況,但這也不是絕對的,因為人都是會犯錯誤的,有時設計缺陷是在所難免的,因此只要不超過兩層的繼承都是可以忍受的。特別是做專案維護的同志,基本上可以不考慮這個規則,為什麼?維護工作基本上都是做擴充套件開發,修復行為,通過一個繼承關係,覆寫一個方法就可以修正一個很大的Bug,何必再要去繼承最高的基類呢?

  • 儘量不要覆寫基類的方法。

     如果基類是一個抽象類,而且這個方法已經實現了,子類儘量不要覆寫。類間依賴的是抽象,覆寫了抽象方法,對依賴的穩定性會產生一定的影響。

  • 結合里氏替換原則使用

     在里氏替換原則中,父類出現的地方子類就能出現,再結合本章節的講解,我們可以得出這樣一個通俗的規則: 介面負責定義public屬性和方法,並且宣告與其他物件的依賴關係,抽象類負責公共構造部分的實現,實現類準確的實現業務邏輯,同時在適當的時候對父類進行細化。

     講了這麼多,估計大家對“倒置”這個詞還是有點不理解,那到底什麼是“倒置”呢?我們先說“正置”是什麼意思,依賴正置就是類間的依賴是實實在在的實現類間的依賴,也就是面向實現程式設計,這也是正常人的思維方式,我要開賓士車就依賴賓士車,我要使用膝上型電腦就直接依賴膝上型電腦,而編寫程式需要的是對現實世界的事物進行抽象,抽象的結果就是有了抽象類和介面,然後我們根據系統設計的需要產生了抽象間的依賴,代替了人們傳統思維中的事物間的依賴,“倒置”就是從這裡產生的。

依賴倒置原則是六個設計原則中最難以實現的原則,它是實現開閉原則的重要途徑,依賴倒置原則沒有實現,就別想實現對擴充套件開放,對修改關閉。在專案中,大家只要記住是“面向介面程式設計”就基本上抓住了依賴倒轉原則的核心。