1. 程式人生 > >依賴注入和Dagger2

依賴注入和Dagger2

1.依賴

如果在 Class A 中,有 Class B 的例項,則稱 Class A 對 Class B 有一個依賴。例如下面類 Human 中用到一個 Father 物件,我們就說類 Human 對類 Father 有一個依賴。

public class Human {
    ...
    Father father;
    ...
    public Human() {
        father = new Father();
    }
}

仔細看這段程式碼我們會發現存在一些問題:

(1). 如果現在要改變 father 生成方式,如需要用new Father(String name)初始化 father,需要修改 Human 程式碼;
(2). 如果想測試不同 Father 物件對 Human 的影響很困難,因為 father 的初始化被寫死在了 Human 的建構函式中;
(3). 如果new Father()過程非常緩慢,單測時我們希望用已經初始化好的 father 物件 Mock 掉這個過程也很困難。

2.依賴注入

上面將依賴在建構函式中直接初始化是一種 Hard init 方式,弊端在於兩個類不夠獨立,不方便測試。我們還有另外一種 Init 方式,如下:

public class Human {
    ...
    Father father;
    ...
    public Human(Father father) {
        this.father = father;
    }
}

上面程式碼中,我們將 father 物件作為建構函式的一個引數傳入。在呼叫 Human 的構造方法之前外部就已經初始化好了 Father 物件。像這種非自己主動初始化依賴,而通過外部來傳入依賴的方式,我們就稱為依賴注入。
現在我們發現上面 1 中存在的兩個問題都很好解決了,簡單的說依賴注入主要有兩個好處:

(1). 解耦,將依賴之間解耦。
(2). 因為已經解耦,所以方便做單元測試,尤其是 Mock 測試。

3. Java 中的依賴注入

依賴注入的實現有多種途徑,而在 Java 中,使用註解是最常用的。通過在欄位的宣告前新增 @Inject 註解進行標記,來實現依賴物件的自動注入。

public class Human {
    ...
    @Inject Father father;
    ...
    public Human() {
    }
}

上面這段程式碼看起來很神奇:只是增加了一個註解,Father 物件就能自動注入了?這個注入過程是怎麼完成的?
實質上,如果你只是寫了一個 @Inject 註解,Father 並不會被自動注入。你還需要使用一個依賴注入框架,並進行簡單的配置。現在 Java 語言中較流行的依賴注入框架有 Google Guice、Spring 等,而在 Android 上比較流行的有 RoboGuice、Dagger 等。其中 Dagger 是我現在正在專案中使用的。 如果感興趣,你可以到 Dagger 實現原理解析 瞭解更多依賴注入和 Dagger 實現原理相關資訊。

4.Dagger2

為什麼是Dagger2

無論是建構函式注入還是介面注入,都避免不了要編寫大量的模板程式碼。機智的猿猿們當然不開心做這些重複性的工作,於是各種依賴注入框架應用而生。但是這麼多的依賴注入框架為什麼我們卻偏愛Dagger2呢?我們先從Spring中的控制反轉(IOC)說起。

談起依賴注入,做過J2EE開發的同學一定會想起Spring IOC,那通過迷之XML來配置依賴的方式真的很讓人討厭;而且XML與Java程式碼分離也導致程式碼鏈難以追蹤。之後更加先進的Guice(Android端也有個RoboGuice)出現了,我們不再需要通過XML來配置依賴,但其執行時實現注入的方式讓我們在追蹤和定位錯誤的時候卻又萬分痛苦。開篇提到過Dagger就是受Guice的啟發而開發出來的;Dagger繼承了前輩的思想,在效能又碾壓了它的前輩Guice,可謂是長江後浪推前浪,前浪死在沙灘上。

又如開篇我在簡介中說到的,Dagger是一種半靜態半執行時的DI框架,雖說依賴注入是完全靜態的,但是生成有向無環圖(DAG)還是基於反射來實現,這無論在大型的服務端應用還是在Android應用上都不是最優方案。升級版的Dagger2解決了這一問題,從半靜態變為完全靜態,從Map式的API變成申明式API(@Module),生成的程式碼更優雅高效;而且一旦出錯我們在編譯期間就能發現。所以Dagger2對開發者的更加友好了,當然Dagger2也因此喪失了一些靈活性,但總體來說利還是遠遠大於弊的。

前面提到這種A B C D E連續依賴的問題,一旦E的建立方式發生了改變就會引發連鎖反應,可能會導致A B C D都需要做針對性的修改;但是騷年,你以為為這僅僅是工作量的問題嗎?更可怕的是我們建立A時需要按順序先建立E D C B四個物件,而且必須保證順序上是正確的。Dagger2就很好的解決了這一問題(不只是Dagger2,在其他DI框架中開發者同樣不需要關注這些問題)。

Dagger2註解

開篇我們就提到Dagger2是基於Java註解來實現依賴注入的,那麼在正式使用之前我們需要先了解下Dagger2中的註解。Dagger2使用過程中我們通常接觸到的註解主要包括:@Inject, @Module, @Provides, @Component, @Qulifier, @Scope, @Singleten。

@Inject:@Inject有兩個作用,一是用來標記需要依賴的變數,以此告訴Dagger2為它提供依賴;二是用來標記建構函式,Dagger2通過@Inject註解可以在需要這個類例項的時候來找到這個建構函式並把相關例項構造出來,以此來為被@Inject標記了的變數提供依賴;

@Module:@Module用於標註提供依賴的類。你可能會有點困惑,上面不是提到用@Inject標記建構函式就可以提供依賴了麼,為什麼還需要@Module?很多時候我們需要提供依賴的建構函式是第三方庫的,我們沒法給它加上@Inject註解,又比如說提供以來的建構函式是帶引數的,如果我們之所簡單的使用@Inject標記它,那麼他的引數又怎麼來呢?@Module正是幫我們解決這些問題的。

@Provides:@Provides用於標註Module所標註的類中的方法,該方法在需要提供依賴時被呼叫,從而把預先提供好的物件當做依賴給標註了@Inject的變數賦值;

@Component:@Component用於標註介面,是依賴需求方和依賴提供方之間的橋樑。被Component標註的介面在編譯時會生成該介面的實現類(如果@Component標註的介面為CarComponent,則編譯期生成的實現類為DaggerCarComponent),我們通過呼叫這個實現類的方法完成注入;

@Qulifier:@Qulifier用於自定義註解,也就是說@Qulifier就如同Java提供的幾種基本元註解一樣用來標記註解類。我們在使用@Module來標註提供依賴的方法時,方法名我們是可以隨便定義的(雖然我們定義方法名一般以provide開頭,但這並不是強制的,只是為了增加可讀性而已)。那麼Dagger2怎麼知道這個方法是為誰提供依賴呢?答案就是返回值的型別,Dagger2根據返回值的型別來決定為哪個被@Inject標記了的變數賦值。但是問題來了,一旦有多個一樣的返回型別Dagger2就懵逼了。@Qulifier的存在正式為了解決這個問題,我們使用@Qulifier來定義自己的註解,然後通過自定義的註解去標註提供依賴的方法和依賴需求方(也就是被@Inject標註的變數),這樣Dagger2就知道為誰提供依賴了。—-一個更為精簡的定義:當型別不足以鑑別一個依賴的時候,我們就可以使用這個註解標示;

@Scope:@Scope同樣用於自定義註解,我能可以通過@Scope自定義的註解來限定註解作用域,實現區域性的單例;

@Singleton:@Singleton其實就是一個通過@Scope定義的註解,我們一般通過它來實現全域性單例。但實際上它並不能提前全域性單例,是否能提供全域性單例還要取決於對應的Component是否為一個全域性物件。

我們提到@Inject和@Module都可以提供依賴,那如果我們即在建構函式上通過標記@Inject提供依賴,有通過@Module提供依賴Dagger2會如何選擇呢?具體規則如下:

步驟1:首先查詢@Module標註的類中是否存在提供依賴的方法。
步驟2:若存在提供依賴的方法,檢視該方法是否存在引數。
a:若存在引數,則按從步驟1開始依次初始化每個引數;
b:若不存在,則直接初始化該類例項,完成一次依賴注入。
步驟3:若不存在提供依賴的方法,則查詢@Inject標註的建構函式,看建構函式是否存在引數。
a:若存在引數,則從步驟1開始依次初始化每一個引數
b:若不存在,則直接初始化該類例項,完成一次依賴注入。

1、案例A

Car類是需求依賴方,依賴了Engine類;因此我們需要在類變數Engine上新增@Inject來告訴Dagger2來為自己提供依賴。

public class Car {

    @Inject
    Engine engine;

    public Car() {
        DaggerCarComponent.builder().build().inject(this);
    }

    public Engine getEngine() {
        return this.engine;
    }
}

Engine類是依賴提供方,因此我們需要在它的建構函式上新增@Inject

public class Engine {

    @Inject
    Engine(){}

    public void run(){
        System.out.println("引擎轉起來了~~~");
    }
}

接下來我們需要建立一個用@Component標註的介面CarComponent,這個CarComponent其實就是一個注入器,這裡用來將Engine注入到Car中。

@Component
public interface CarComponent {
    void inject(Car car);
}

完成這些之後我們需要Build下專案,讓Dagger2幫我們生成相關的Java類。接著我們就可以在Car的建構函式中呼叫Dagger2生成的DaggerCarComponent來實現注入(這其實在前面Car類的程式碼中已經有了體現)

public Car() {
    DaggerCarComponent.builder().build().inject(this);
}
2、案例B

如果建立Engine的建構函式是帶引數的呢?比如說製造一臺引擎是需要齒輪(Gear)的。或者Eggine類是我們無法修改的呢?這時候就需要@Module和@Provide上場了。

同樣我們需要在Car類的成員變數Engine上加上@Inject表示自己需要Dagger2為自己提供依賴;Engine類的建構函式上的@Inject也需要去掉,應為現在不需要通過建構函式上的@Inject來提供依賴了。

public class Car {

    @Inject
    Engine engine;

    public Car() {
        DaggerCarComponent.builder().markCarModule(new MarkCarModule())
                .build().inject(this);
    }

    public Engine getEngine() {
        return this.engine;
    }
}

接著我們需要一個Module類來生成依賴物件。前面介紹的@Module就是用來標準這個類的,而@Provide則是用來標註具體提供依賴物件的方法(這裡有個不成文的規定,被@Provide標註的方法命名我們一般以provide開頭,這並不是強制的但有益於提升程式碼的可讀性)。

@Module
public class MarkCarModule {

    public MarkCarModule(){ }

    @Provides Engine provideEngine(){
        return new Engine("gear");
    }
}

接下來我們還需要對CarComponent進行一點點修改,之前的@Component註解是不帶引數的,現在我們需要加上modules = {MarkCarModule.class},用來告訴Dagger2提供依賴的是MarkCarModule這個類。

@Component(modules = {MarkCarModule.class})
public interface CarComponent {
    void inject(Car car);
}

Car類的建構函式我們也需要修改,相比之前多了個markCarModule(new MarkCarModule())方法,這就相當於告訴了注入器DaggerCarComponent把MarkCarModule提供的依賴注入到了Car類中。

public Car() {
   DaggerCarComponent.builder()
           .markCarModule(new MarkCarModule())
           .build().inject(this);
}

這樣一個最最基本的依賴注入就完成了,接下來我們測試下我們的程式碼。

public static void main(String[] args){
    Car car = new Car();
    car.getEngine().run();
}
輸出

引擎轉起來了~~~