面試官:你是如何理解Java中依賴倒置和依賴注入以及控制反轉的?

享學課堂online發表於2019-05-02

什麼是依賴(Dependency)?

依賴是類與類之間的連線,依賴關係表示一個類依賴於另一個類的定義,通俗來講

就是一種需要,例如一個人(Person)可以買車(Car)和房子(House),Person類依賴於Car類和House類

public static void main(String ... args){
        //TODO:

    Person person = new Person();
    person.buy(new House());
    person.buy(new Car());

}

static class Person{

    //表示依賴House
    public void buy(House house){}
    //表示依賴Car
    public void buy(Car car){}
}

static class House{

}

static class Car{

}
複製程式碼

依賴倒置 (Dependency inversion principle)

依賴倒置是物件導向設計領域的一種軟體設計原則

軟體設計有 6 大設計原則,合稱 SOLID

1、單一職責原則(Single Responsibility Principle,簡稱SRP )

  • 核心思想: 應該有且僅有一個原因引起類的變更
  • 問題描述: 假如有類Class1完成職責T1,T2,當職責T1或T2有變更需要修改時,有可能影響到該類的另外一個職責正常工作。
  • 好處: 類的複雜度降低、可讀性提高、可維護性提高、擴充套件性提高、降低了變更引起的風險。
  • 需注意: 單一職責原則提出了一個編寫程式的標準,用“職責”或“變化原因”來衡量介面或類設計得是否優良,但是“職責”和“變化原因”都是不可以度量的,因專案和環境而異。

2、里氏替換原則(Liskov Substitution Principle,簡稱LSP)

  • 核心思想: 在使用基類的的地方可以任意使用其子類,能保證子類完美替換基類。
  • 通俗來講: 只要父類能出現的地方子類就能出現。反之,父類則未必能勝任。
  • 好處: 增強程式的健壯性,即使增加了子類,原有的子類還可以繼續執行。
  • 需注意: 如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關係 採用依賴、聚合、組合等關係代替繼承。

3、依賴倒置原則(Dependence Inversion Principle,簡稱DIP)

  • 核心思想:高層模組不應該依賴底層模組,二者都該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象;
  • 說明:高層模組就是呼叫端,低層模組就是具體實現類。抽象就是指介面或抽象類。細節就是實現類。
  • 通俗來講: 依賴倒置原則的本質就是通過抽象(介面或抽象類)使個各類或模組的實現彼此獨立,互不影響,實現模組間的鬆耦合。
  • 問題描述: 類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的程式碼來達成。這種場景下,類A一般是高層模組,負責複雜的業務邏輯;類B和類C是低層模組,負責基本的原子操作;假如修改類A,會給程式帶來不必要的風險。
  • 解決方案: 將類A修改為依賴介面interface,類B和類C各自實現介面interface,類A通過介面interface間接與類B或者類C發生聯絡,則會大大降低修改類A的機率。
  • 好處:依賴倒置的好處在小型專案中很難體現出來。但在大中型專案中可以減少需求變化引起的工作量。使並行開發更友好。

4、介面隔離原則(Interface Segregation Principle,簡稱ISP)

  • 核心思想:類間的依賴關係應該建立在最小的介面上
  • 通俗來講: 建立單一介面,不要建立龐大臃腫的介面,儘量細化介面,介面中的方法儘量少。也就是說,我們要為各個類建立專用的介面,而不要試圖去建立一個很龐大的介面供所有依賴它的類去呼叫。
  • 問題描述: 類A通過介面interface依賴類B,類C通過介面interface依賴類D,如果介面interface對於類A和類B來說不是最小介面,則類B和類D必須去實現他們不需要的方法。
  • 需注意:
  • 介面儘量小,但是要有限度。對介面進行細化可以提高程式設計靈活性,但是如果過小,則會造成介面數量過多,使設計複雜化。所以一定要適度
  • 提高內聚,減少對外互動。使介面用最少的方法去完成最多的事情
  • 為依賴介面的類定製服務。只暴露給呼叫的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模組提供定製服務,才能建立最小的依賴關係。

5、迪米特法則(Law of Demeter,簡稱LoD)

  • 核心思想: 類間解耦。
  • 通俗來講: 一個類對自己依賴的類知道的越少越好。自從我們接觸程式設計開始,就知道了軟體程式設計的總的原則:低耦合,高內聚。無論是程式導向程式設計還是物件導向程式設計,只有使各個模組之間的耦合儘量的低,才能提高程式碼的複用率。低耦合的優點不言而喻,但是怎麼樣程式設計才能做到低耦合呢?那正是迪米特法則要去完成的。

6、開放封閉原則(Open Close Principle,簡稱OCP)

  • 核心思想: 儘量通過擴充套件軟體實體來解決需求變化,而不是通過修改已有的程式碼來完成變化
  • 通俗來講: 一個軟體產品在生命週期內,都會發生變化,既然變化是一個既定的事實,我們就應該在設計的時候儘量適應這些變化,以提高專案的穩定性和靈活性。

依賴倒置原則的定義如下:

  • 上層模組不應該依賴底層模組,它們都應該依賴於抽象。
  • 抽象不應該依賴於細節,細節應該依賴於抽象。

什麼是上層模組和底層模組?

不管你承認不承認,“有人的地方就有江湖”,我們都說人人平等,但是對於任何一個組織機構而言,它一定有架構的設計有職能的劃分。按照職能的重要性,自然而然就有了上下之分。並且,隨著模組的粒度劃分不同這種上層與底層模組會進行變動,也許某一模組相對於另外一模組它是底層,但是相對於其他模組它又可能是上層

組織架構

面試官:你是如何理解Java中依賴倒置和依賴注入以及控制反轉的?
公司管理層就是上層,CEO 是整個事業群的上層,那麼 CEO 職能之下就是底層。

然後,我們再以事業群為整個體系劃分模組,各個部門經理以上部分是上層,那麼之下的組織都可以稱為底層。

由此,我們可以看到,在一個特定體系中,上層模組與底層模組可以按照決策能力高低為準繩進行劃分。

那麼,對映到我們軟體實際開發中,一般我們也會將軟體進行模組劃分,比如業務層、邏輯層和資料層。

業務模組

面試官:你是如何理解Java中依賴倒置和依賴注入以及控制反轉的?
業務層中是軟體真正要進行的操作,也就是做什麼

邏輯層是軟體現階段為了業務層的需求提供的實現細節,也就是怎麼做

資料層指業務層和邏輯層所需要的資料模型。

因此,如前面所總結,按照決策能力的高低進行模組劃分。業務層自然就處於上層模組,邏輯層和資料層自然就歸類為底層。

什麼是抽象和細節?

象如其名字一樣,是一件很抽象的事物。抽象往往是相對於具體而言的,具體也可以被稱為細節,當然也被稱為具象。

比如:

  1. 這是一幅畫。畫是抽象,而油畫、素描、國畫而言就是具體。
  2. 這是一件藝術品,藝術品是抽象,而畫、照片、瓷器等等就是具體了。
  3. 交通工具是抽象,而公交車、單車、火車等就是具體了。
  4. 表演是抽象,而唱歌、跳舞、小品等就是具體。

上面可以知道,抽象可以是物也可以是行為。

具體對映到軟體開發中,抽象可以是介面或者抽象類形式。

 /**
 * Driveable 是介面,所以它是抽象
 */
public interface Driveable {
    void drive();
}
/**
 * 而 Bike 實現了介面,它們被稱為具體。
 */
public class Bike implements Driveable {
    @Override
    public void drive() {
        System.out.println("Bike drive");
    }
}
/**
 * 而 Car實現了介面,它們被稱為具體。
 */
public class Car implements Driveable {
    @Override
    public void drive() {
        System.out.println("Car drive.");
    }
}
複製程式碼

依賴倒置的好處

在平常的開發中,我們大概都會這樣編碼。

public class Person {

    private Bike mBike;
    private Car mCar;
    private Train mTrain;

    public Person(){
        mBike = new Bike();
        //mCar = new Car();
//        mTrain = new Train();
    }

    public void goOut(){
        System.out.println("出門啦");
        mBike.drive();
        //mCar.drive();
//        mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person person = new Person();
        person.goOut();
    }
}
複製程式碼

我們建立了一個 Person 類,它擁有一臺自行車,出門的時候就騎自行車。

不過,自行車適應很短的距離。如果,我要出門逛街呢?自行車就不大合適了。於是就要改成汽車。

不過,如果我要到北京去,那麼汽車也不合適了。

有沒有一種方法能讓 Person 的變動少一點呢?因為這是最基礎的演示程式碼,如果工程大了,程式碼複雜了,Person 面對需求變動時改動的地方會更多。

而依賴倒置原則正好適用於解決這類情況。

下面,我們嘗試運用依賴倒置原則對程式碼進行改造。

我們再次回顧下它的定義。

上層模組不應該依賴底層模組,它們都應該依賴於抽象。

抽象不應該依賴於細節,細節應該依賴於抽象。

首先是上層模組和底層模組的拆分。

按照決策能力高低或者重要性劃分,Person 屬於上層模組,Bike、Car 和 Train 屬於底層模組。

上層模組不應該依賴於底層模組。

person架構

面試官:你是如何理解Java中依賴倒置和依賴注入以及控制反轉的?

public class Person {

//    private Bike mBike;
    private Car mCar;
    private Train mTrain;
    private Driveable mDriveable;

    public Person(){
//        mBike = new Bike();
        //mCar = new Car();
       mDriveable = new Train();
    }

    public void goOut(){
        System.out.println("出門啦");
        mDriveable.drive();
        //mCar.drive();
//        mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person person = new Person();
        person.goOut();
    }
}
複製程式碼

可以看到,依賴倒置實質上是面向介面程式設計的體現

控制反轉 (IoC)

控制反轉 IoC 是 Inversion of Control的縮寫,意思就是對於控制權的反轉,對麼控制權是什麼控制權呢?

Person自己掌控著內部 mDriveable 的例項化。

現在,我們可以更改一種方式。將 mDriveable 的例項化移到 Person 外面。

public class Person2 {

    private Driveable mDriveable;

    public Person2(Driveable driveable){
        this.mDriveable = driveable;
    }

    public void goOut(){
        System.out.println("出門啦");
        mDriveable.drive();
        //mCar.drive();
//        mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person2 person = new Person2(new Car());
        person.goOut();
    }
}
複製程式碼

就這樣無論出行方式怎麼變化,Person 這個類都不需要更改程式碼了。

在上面程式碼中,Person 把內部依賴的建立權力移交給了 Person2這個類中的 main() 方法。也就是說 Person 只關心依賴提供的功能,但並不關心依賴的建立。

這種思想其實就是 IoC,IoC 是一種新的設計模式,它對上層模組與底層模組進行了更進一步的解耦。控制反轉的意思是反轉了上層模組對於底層模組的依賴控制。

比如上面程式碼,Person 不再親自建立 Driveable 物件,它將依賴的例項化的權力交接給了 Person2。而 Person2在 IoC 中又指代了 IoC 容器 這個概念。

依賴注入(Dependency injection)

依賴注入,也經常被簡稱為 DI,其實在上一節中,我們已經見到了它的身影。它是一種實現 IoC 的手段。什麼意思呢?

為了不因為依賴實現的變動而去修改 Person,也就是說以可能在 Driveable 實現類的改變下不改動 Person 這個類的程式碼,儘可能減少兩者之間的耦合。我們需要採用上一節介紹的 IoC 模式來進行改寫程式碼。

這個需要我們移交出對於依賴例項化的控制權,那麼依賴怎麼辦?Person 無法例項化依賴了,它就需要在外部(IoC 容器)賦值給它,這個賦值的動作有個專門的術語叫做注入(injection),需要注意的是在 IoC 概念中,這個注入依賴的地方被稱為 IoC 容器,但在依賴注入概念中,一般被稱為注射器 (injector)。

表達通俗一點就是:我不想自己例項化依賴,你(injector)建立它們,然後在合適的時候注入給我

實現依賴注入有 3 種方式:

  1. 建構函式中注入
  2. setter 方式注入
  3. 介面注入
/**
 * 介面方式注入
 * 介面的存在,表明了一種依賴配置的能力。
 */
public interface DepedencySetter {
    void set(Driveable driveable);
}
public class Person2  implements DepedencySetter {

    //介面方式注入
    @Override
    public void set(Driveable driveable) {
        this.mDriveable = mDriveable;
    }

    private Driveable mDriveable;

    //建構函式注入
    public Person2(Driveable driveable){
        this.mDriveable = driveable;
    }

    //setter 方式注入
    public void setDriveable(Driveable mDriveable) {
        this.mDriveable = mDriveable;
    }

    public void goOut(){
        System.out.println("出門啦");
        mDriveable.drive();
        //mCar.drive();
//        mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person2 person = new Person2(new Car());
        person.goOut();
    }
}
複製程式碼

最後

讀到這的朋友關注下,以後會不停更新更多精選乾貨及資訊分享,歡迎大家在評論區留言討論!

歡迎關注享學課堂online微信公眾號,每天會持續更新技術乾貨,熱點,吐槽等文章,還有免費的Java架構視訊資料和麵試專題資料免費領取分享,後臺回覆關鍵字【Java資料】,免費獲取Java架構面試專題文件資料、電子書及更多架構進階視訊資料(視訊+筆記)

面試官:你是如何理解Java中依賴倒置和依賴注入以及控制反轉的?



相關文章