【轉載】淺析依賴倒置(DIP)、控制反轉(IOC)和依賴注入(DI)

原文地址 http://blog.csdn.net/briblue/article/details/75093382程序員

寫這篇文章的緣由是這兩天在編寫關於 Dagger2 主題的博文時,花了大量的精力來解釋依賴注入這個概念。後來想一下,這些在面向對象開發過程當中與依賴相關的諸多術語和概念實際狀況下很是的抽象,所以獨立成文也有就必定的意義,旨在幫助初學者快速地對這些概念有一個大體的印象,而後經過一些實例來領悟它們之中的內在奧祕。編程

什麼是依賴(Dependency)?

依賴是一種關係,通俗來說就是一種須要。
這裏寫圖片描述設計模式

程序員須要電腦,由於沒有電腦程序員就沒有辦法編寫代碼,因此說程序員依賴電腦,電腦被程序員依賴。架構

在面向對象編程中,代碼能夠這樣編寫。框架

class Coder {

    Computer mComputer;

    public Coder () {
        mComputer = new Computer();
    }
}

Coder 的內部持有 Computer 的引用,這就是依賴在編程世界中的體現。ide

依賴倒置 (Dependency inversion principle)

依賴倒置是面向對象設計領域的一種軟件設計原則。
軟件設計有 6 大設計原則,合稱 SOLID函數

有人會有疑惑,設計原則有什麼用呢?工具

設計原則是前輩們總結出來的經驗,你能夠把它們看做是內功心法。學習

只要你在日常開發中按照設計原則進行編碼,假以時日,你編程的功力將會大增。測試

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

  1. 上層模塊不該該依賴底層模塊,它們都應該依賴於抽象。
  2. 抽象不該該依賴於細節,細節應該依賴於抽象。

乍一看,這會讓初學者摸不清頭腦。這種學術性的歸納語言近乎於軟件行業中的哲學。可實質上,它確實稱得上是哲學,如今 SOLID 幾乎等同於面向對象開發中的金科玉律,可是也正由於它的高度歸納、它的晦澀難懂,對於廣大初學者而言這是一件很是不友好的事物。

咱們該怎麼理解上面的定義呢?咱們須要咬文嚼字,各個突破。

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

無論你認可不認可,「有人的地方就有江湖」,咱們都說人人平等,可是對於任何一個組織機構而言,它必定有架構的設計有職能的劃分。按照職能的重要性,天然而然就有了上下之分。而且,隨着模塊的粒度劃分不一樣這種上層與底層模塊會進行變更,也許某一模塊相對於另一模塊它是底層,可是相對於其餘模塊它又多是上層。

這裏寫圖片描述

公司管理層就是上層,CEO 是整個事業羣的上層,那麼 CEO 職能之下就是底層。

而後,咱們再以事業羣爲整個體系劃分模塊,各個部門經理以上部分是上層,那麼之下的組織均可以稱爲底層。

由此,咱們能夠看到,在一個特定體系中,上層模塊與底層模塊能夠按照決策能力高低爲準繩進行劃分。

那麼,映射到咱們軟件實際開發中,通常咱們也會將軟件進行模塊劃分,好比業務層、邏輯層和數據層。
這裏寫圖片描述

業務層中是軟件真正要進行的操做,也就是作什麼
邏輯層是軟件現階段爲了業務層的需求提供的實現細節,也就是怎麼作
數據層指業務層和邏輯層所須要的數據模型。

所以,如前面所總結,按照決策能力的高低進行模塊劃分。業務層天然就處於上層模塊,邏輯層和數據層天然就歸類爲底層。
這裏寫圖片描述

什麼是抽象和細節?

抽象如其名字同樣,是一件很抽象的事物。抽象每每是相對於具體而言的,具體也能夠被稱爲細節,固然也被稱爲具象。

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

上面能夠知道,抽象能夠是物也能夠是行爲。

具體映射到軟件開發中,抽象能夠是接口或者抽象類形式。

public interface Driveable {
    void drive();
}

class Bike implements Driveable{

    @Override
    public void drive() {
        // TODO Auto-generated method stub
        System.out.println("Bike drive.");
    }

}

class Car implements Driveable{

    @Override
    public void drive() {
        // TODO Auto-generated method stub
        System.out.println("Car drive.");
    }

}

Driveable 是接口,因此它是抽象,而 Bike 和 Car 實現了接口,它們被稱爲具體。

如今,咱們理解了依賴、上層模塊、底層模塊、抽象和具體。這樣咱們能夠正式開始學習依賴倒置原理這個概念了?

依賴倒置的好處

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

public class Person {

    private Bike mBike;

    public Person() {
        mBike = new Bike();
    }

    public void chumen() {
        System.out.println("出門了");
        mBike.drive();
    }

}

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

public class Test1 {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Person person = new Person();

        person.chumen();

    }

}

執行結果以下:

出門了
Bike drive.

不過,自行車適應很短的距離。若是,我要出門逛街呢?自行車就不大合適了。因而就要改爲汽車。

public class Person {

    private Bike mBike;
    private Car mCar;

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

    public void chumen() {
        System.out.println("出門了");
        //mBike.drive();
        mCar.drive();
    }

}

咱們須要修改 Person 這個類的代碼。

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

class Train implements Driveable{
    @Override
    public void drive() {
        // TODO Auto-generated method stub
        System.out.println("Train drive.");
    }
}

package com.frank.test;

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 chumen() {
        System.out.println("出門了");
        //mBike.drive();
        //mCar.drive();
        mTrain.drive();
    }

}

咱們添加了 Train 這個最新的實現類,而後再次修改了 Person 這個類。

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

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

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

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

  1. 上層模塊不該該依賴底層模塊,它們都應該依賴於抽象。
  2. 抽象不該該依賴於細節,細節應該依賴於抽象。

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

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

上層模塊不該該依賴於底層模塊。
可是

public class Person {

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

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

}

Person 這個類顯然是依賴於 Bike 和 Car。Person 類中 chumen() 的能力徹底依賴於屬性 Bike 或者 Car 對象,也就是說 Person 把本身的能力依賴在 Bike 和 Car 身上。

上層和底層都應該依賴於抽象。

這裏寫圖片描述

咱們的代碼中,Person 沒有依賴抽象,因此咱們得引進抽象。

而底層的抽象是什麼,是 Driveable 這個接口。

public class Person {

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

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

    public void chumen() {
        System.out.println("出門了");
        //mBike.drive();
        //mCar.drive();
        //mTrain.drive();
        mDriveable.drive();
    }

}

執行結果以下:

出門了
Train drive.

如今,Person 類中 chumen() 這個方法依賴於 Driveable 接口的抽象,它沒有限定本身出行的可能性,任何 Car、Bike 或者是 Train 均可以的。

到這一步,咱們能夠說是符合了上層不依賴於底層,依賴於抽象的準則了。

那麼,抽象不該該依賴於細節,細節應該依賴於抽象又是什麼意思呢?

以上面爲例,Driveable 是抽象,它表明一種行爲,而 Bike、Car、Train 都是實現細節。

Person 須要的是 Driveable,須要的是交通工具,但不是說交通工具必定是 Bike、Car、Train。將來也多是 AirPlane。

class AirPlane implements Driveable{
    @Override
    public void drive() {
        // TODO Auto-generated method stub
        System.out.println("AirPlane fly.");
    }
}

那麼一個 Person,它下次出門改爲飛機能夠嗎?固然能夠的。由於依賴倒置的原因,Person 展示出了極度的可擴展性。

這裏寫圖片描述

上面的內容就是依賴倒置原則。

有人會考慮到倒置這個詞,我的的理解是倒置是改變的意思。

原本正常編碼下,確定會出現上層依賴底層的狀況,而依賴倒置原則的應用則改變了它們之間依賴的關係,它引進了抽象。上層依賴於抽象,底層的實現細節也依賴於抽象,因此依賴倒置咱們能夠理解爲依賴關係被改變,若是很是糾結於倒置這個詞,那麼倒置的實際上是底層細節,本來它是被上層依賴,如今它倒要依賴與抽象的接口。

這裏寫圖片描述

能夠看到,依賴倒置實質上是面向接口編程的體現

控制反轉 (IoC)

控制反轉 IoC 是 Inversion of Control的縮寫,意思就是對於控制權的反轉,對麼控制權是什麼控制權呢?
你們從新審視上面的代碼。

public class Person {

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

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

    public void chumen() {
        System.out.println("出門了");
        //mBike.drive();
        //mCar.drive();
        //mTrain.drive();
        mDriveable.drive();
    }

}

雖然,chumen() 這個方法再也不由於出行方式的改變而變更,可是每次更改出行方式的時候,Person 這個類仍是要修改。

Person 類仍是要實例化 mDriveable 的接口對象。

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

Person 本身掌控着內部 mDriveable 的實例化。
如今,咱們能夠更改一種方式。將 mDriveable 的實例化移到 Person 外面。

public class Person {

    private Driveable mDriveable;

    public Person(Driveable driveable) {

        this.mDriveable = driveable;
    }

    public void chumen() {
        System.out.println("出門了");

        mDriveable.drive();
    }

}

就這樣不管出行方式怎麼變化,Person 這個類都不須要更改代碼了。

public class Test1 {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Bike bike = new Bike();
        Car car = new Car();
        Train train = new Train();
//      Person person = new Person(bike);
//      Person person = new Person(car);
        Person person = new Person(train);

        person.chumen();

    }

}

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

這種思想其實就是 IoC,IoC 是一種新的設計模式,它對上層模塊與底層模塊進行了更進一步的解耦。控制反轉的意思是反轉了上層模塊對於底層模塊的依賴控制。
好比上面代碼,Person 再也不親自建立 Driveable 對象,它將依賴的實例化的權力交接給了 Test1。而 Test1 在 IoC 中又指代了 IoC 容器 這個概念。

再舉一個例子,咱們到餐廳去叫外賣,餐廳有專門送外賣的外賣員,他們的使命就是及時送達外賣食品。

依照依賴倒置原則,咱們能夠建立這樣一個類。

public abstract class WaimaiYuan {

    protected Food food;

    public WaimaiYuan(Food food) {
        this.food = food;
    }

    abstract void songWaiMai();

}

public class Xiaohuozi extends WaimaiYuan {

    public Xiaohuozi(Food food) {
        super(food);
    }

    @Override
    void songWaiMai() {
        System.out.println("我是小夥子,爲您送的外賣是:"+food);

    }

}

public class XiaoGuniang extends WaimaiYuan {

    public XiaoGuniang(Food food) {
        super(food);
    }

    @Override
    void songWaiMai() {
        System.out.println("我是小姑娘,爲您送的外賣是:"+food);
    }

}

WaimaiYuan 是抽象類,表明送外賣的,Xiaohuozi 和 XiaoGuniang 是它的繼承者,說明他們均可以送外賣。WaimaiYuan 都依賴於 Food,可是它沒有實例化 Food 的權力。

再編寫食物類代碼

public abstract class Food {
    protected String name;

    @Override
    public String toString() {
        return name;
    }

}

public class PijiuYa extends Food {

    public PijiuYa() {
        name = "啤酒鴨";
    }

}

public class DuojiaoYutou extends Food {

    public DuojiaoYutou() {
        name = "剁椒魚頭";
    }

}

Food 是抽象類,PijiuYa 和 DuojiaoYutou 都是實現細節。

IoC 少不了 IoC 容器,也就是實例化抽象的地方。咱們編寫一個餐廳類。

public class Restaurant {

    public static void peican(int orderid,int flowid) {
        WaimaiYuan person;
        Food food;

        if ( orderid == 0) {
            food = new PijiuYa();
        } else {
            food = new DuojiaoYutou();
        }

        if ( flowid % 2 == 0 ) {
            person = new Xiaohuozi(food);
        } else {
            person = new XiaoGuniang(food);
        }

        person.songWaiMai();

    }

}

orderid 表明菜品編號,0 是啤酒鴨,其它則是剁椒魚頭。
flowid 是訂單的流水號碼。 餐廳根據流水編碼的不一樣來指派小夥子或者小姑娘來送外賣,編寫測試代碼。

public class IocTest {

    public static void main(String[] args) {

        Restaurant.peican(0, 0);
        Restaurant.peican(0, 1);
        Restaurant.peican(1, 2);
        Restaurant.peican(0, 3);
        Restaurant.peican(1, 4);
        Restaurant.peican(1, 5);
        Restaurant.peican(1, 6);
        Restaurant.peican(0, 7);
        Restaurant.peican(0, 8);
    }

}

餐廳一次性送了 9 份外賣。

我是小夥子,爲您送的外賣是:啤酒鴨
我是小姑娘,爲您送的外賣是:啤酒鴨
我是小夥子,爲您送的外賣是:剁椒魚頭
我是小姑娘,爲您送的外賣是:啤酒鴨
我是小夥子,爲您送的外賣是:剁椒魚頭
我是小姑娘,爲您送的外賣是:剁椒魚頭
我是小夥子,爲您送的外賣是:剁椒魚頭
我是小姑娘,爲您送的外賣是:啤酒鴨
我是小夥子,爲您送的外賣是:啤酒鴨

能夠看到的是,由於有 Restaurant 這個 IoC 容器存在,大大地解放了外賣員的生產力,外賣員再也不依賴具體的食物,具體的食物也再也不依賴於特定的外賣員。也就是說,只要是食物外賣員就能夠送,任何一種食物能夠被任何一位外賣員送。

你們細細體會這是怎麼樣一種靈活性。若是非要外賣員本身決定配送什麼食物,人少則還行,人多的時候,訂單多的時候確定會亂成一鍋粥。

因此,實際工做當中,基本上都是按照專業的人幹專業的事這種基本規律運行。外賣員沒有能力也沒有義務去親自決定該送什麼訂單,這種權力在於餐廳,只要餐廳配置好就 OK 了。

記住 配置 這個詞。

在軟件開發領域,相似餐廳這種調度配置而後決定依賴關係的 IOC 容器有許多框架好比 Spring。可是,因爲我自己是 Android 開發的,對於 Spring 知之甚少,因此對這一塊不作過多介紹。

但做爲 IoC 容器,無非是針對配置而後動態生成依賴關係。有的配置是開發者按照規則編寫在 xml 格式文件中,有些配置則是利用 Java 中的反射與註解。

IoC 模式最核心的地方就是在於依賴方與被依賴方之間,也就是上文中說的上層模塊與底層模塊之間引入了第三方,這個第三方統稱爲 IoC 容器,由於 IoC 容器的介入,致使上層模塊對於它的依賴的實例化控制權發生變化,也就是所謂的控制反轉的意思。

總之,由於 IoC 容器的存在,使得開發者編寫大型系統工程的時候極大地解放了生產力。

依賴注入(Dependency injection)

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

public class Person {

    private Driveable mDriveable;

    public Person() {

        mDriveable = new Train();

    }

    public void chumen() {
        System.out.println("出門了");

        mDriveable.drive();
    }

}

咱們再回顧 Person 這個類。在構造 Person 的時候,Person 內部初始化了 Driveable 對象,選擇了 Train() 爲實現,這種編碼方式太具備侷限性了。下次選擇其它出行方式如 Bike 或者 Car 的時候,Person 這個類須要修改。

public class Person {

    private Driveable mDriveable;

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

    }

    public void chumen() {
        System.out.println("出門了");

        mDriveable.drive();
    }

}

爲了避免由於依賴實現的變更而去修改 Person,也就是說以可能在 Driveable 實現類的改變下不改動 Person 這個類的代碼,儘量減小二者之間的耦合。咱們須要採用上一節介紹的 IoC 模式來進行改寫代碼。

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

表達通俗一點就是:我不想本身實例化依賴,你(injector)建立它們,而後在合適的時候注入給我吧。

再好比顧客去餐廳須要碗筷,可是顧客不須要本身帶碗筷去,因此,在點菜的時候和服務員說,你給我一副碗筷吧。在這個場景中若是按照正常的編程方式,碗筷自己是顧客的依賴,可是應用 IoC 模式以後 ,碗筷是服務員提供(注入)給顧客的,顧客不用關心吃飯的時候用什麼碗筷,由於吃不一樣的菜品,可能餐具不一樣,吃牛排用刀叉,喝湯用調羹,雖然顧客就餐時須要餐具,可是餐具的配置應該交給餐廳的工做人員。

這裏寫圖片描述

若是以軟件角度來描述,餐具是顧客是依賴,服務員給顧客配置餐具的過程就是依賴注入。

上一節的外賣員和菜品的例子,其實也是依賴注入的例子。

實現依賴注入有 3 種方式:
1. 構造函數中注入
2. setter 方式注入
3. 接口注入

咱們如今一一觀察這些方式

構造函數注入

public class Person {

    private Driveable mDriveable;

    public Person(Driveable driveable) {

        this.mDriveable = driveable;
    }

    public void chumen() {
        System.out.println("出門了");

        mDriveable.drive();
    }

}

優勢:在 Person 一開始建立的時候就肯定好了依賴。
缺點:後期沒法更改依賴。

setter 方式注入

public class Person {

    private Driveable mDriveable;

    public Person() {

    }

    public void chumen() {
        System.out.println("出門了");

        mDriveable.drive();
    }

    public void setDriveable(Driveable mDriveable) {
        this.mDriveable = mDriveable;
    }

}

優勢:Person 對象在運行過程當中能夠靈活地更改依賴。
缺點:Person 對象運行時,可能會存在依賴項爲 null 的狀況,因此須要檢測依賴項的狀態。

public void chumen() {

    if ( mDriveable != null ) {
        System.out.println("出門了");
        mDriveable.drive();
    }

    }

接口方式注入

public interface DepedencySetter {
    void set(Driveable driveable);
}

class Person implements DepedencySetter{
    private Driveable mDriveable;

    public void chumen() {

        if ( mDriveable != null ) {
            System.out.println("出門了");
            mDriveable.drive();
        }

    }

    @Override
    public void set(Driveable driveable) {
        this.mDriveable = mDriveable;
    }

}

這種方式和 Setter 方式很類似。有不少同窗可能有疑問那麼加入一個接口是否是畫蛇添足呢?

答案確定是否是的,這涉及到一個角色的問題。仍是之前面的餐廳爲例,除了外賣員以外還有廚師和服務員,那麼若是隻有外賣員實現了一個送外賣的接口的話,那麼餐廳配餐的時候就只會把外賣配置給外賣員。

接口的存在,代表了一種依賴配置的能力。

在軟件框架中,讀取 xml 配置文件,或者是利用反射技術讀取註解,而後根據配置信息,框架動態將一些依賴配置給特定接口的類,咱們也能夠說 Injector 也依賴於接口,而不是特定的實現類,這樣進一步提升了準確性與靈活性。

總結

  1. 依賴倒置是面向對象開發領域中的軟件設計原則,它倡導上層模塊不依賴於底層模塊,抽象不依賴細節。
  2. 依賴反轉是遵照依賴倒置這個原則而提出來的一種設計模式,它引入了 IoC 容器的概念。
  3. 依賴注入是爲了實現依賴反轉的一種手段之一。
  4. 它們的本質是爲了代碼更加的「高內聚,低耦合」

這裏寫圖片描述

這篇文章我運用了大量的比喻來解釋讓給概念,我相信可以加深讀者們對於這些概念的理解。

若是要獲取更全面的信息,你們能夠查看維基百科相關的頁面。
Dependency inversion principle
Inversion_of_control
Dependency injection

相關文章
相關標籤/搜索