本篇代碼收錄在項目的chapter2分支
接下來我們要討論依賴關(guān)系及其解決方案,在這之前我們約定幾個名詞和符號,在第二篇的例子當(dāng)中,我們知道Car依賴Engine,這是最簡單的依賴關(guān)系模型,A依賴B,我們把A叫做需求方,B叫需求對象。
所以我們約定幾個名詞和符號:
依賴模型的定義
- 需求方R(requirement)
- 需求對象O(object)
- 依賴關(guān)系D(dependency)
在表達式里也使用符號 -->來表示依賴方向,如果A依賴B,則可以表示為A --D(a,b)--> B,如果不強調(diào)Dab的存在,則可以簡化為A-->B。
那么一個最簡單的依賴模型可以表示為:
R--D(r,o)-->O
把我們例子中的類代入就得到:
R(Car)--D(Car,Engine)-->O(Engine)
如果我們在Car類的構(gòu)造方法里直接創(chuàng)建Engine對象,這種實現(xiàn)就是上述的一個依賴模型的一比一還原。通過之前的討論,我們知道D(car,engine)這種對象的依賴關(guān)系是會隨著軟件的規(guī)模遞增,變得復(fù)雜難以維護的,所以我們需要使用依賴注入的方式來滿足需求。后面我們對它進行了一個優(yōu)化,在入口處動態(tài)創(chuàng)建Engine對象并傳遞給Car對象。那么在這個優(yōu)化過程當(dāng)中,實際上增加了一個角色:Object的提供者。所以我們增加一個定義:
- 需求提供方P(provider)
那么上述的依賴模型就變成了:
R--D(r,p)-->P--D(p,o)-->O
代入我們例子中的類:
R(Car)--D(Car,Main)-->P(Main)--D(Main,Engine)-->O(Engine)
依賴注入及優(yōu)化的模型本質(zhì)
我們發(fā)現(xiàn)依賴模型更加復(fù)雜了,是的,不管我們在編程中使用手動實現(xiàn)依賴注入也好,還是通過dagger2等工具實現(xiàn)依賴注入也好,其背后都會造成依賴模型的復(fù)雜化,這帶來的一個弊端就是會導(dǎo)致我們的軟件系統(tǒng)更加難以理解,邏輯更加難以跟蹤,因為我們除了要理解業(yè)務(wù)邏輯之外,還要理解依賴注入的過程。
但是我們看一下上述的優(yōu)化模型,其中的D(car,engin),已經(jīng)被D(car,main)+D(main,engin)替換了。也就是D(r,o)=D(r,p)+D(p,o)。我們注意到main是我們程序的入口,其層級為1,而car的構(gòu)造函數(shù)是我們程序的內(nèi)部邏輯,其層級為2。也就是說我們通過這樣一個代換,雖然增加了依賴關(guān)系的模型,但是降低了依賴層級。這種依賴層級的降低,使得軟件系統(tǒng)結(jié)構(gòu)更加地松散、靈活。如果使用dagger2等依賴注入工具,最終我們可以將依賴層級降為0,即不使用代碼來維護依賴關(guān)系,而是使用注解,從而避免因依賴關(guān)系變更導(dǎo)致的軟件系統(tǒng)的變更,實現(xiàn)基于注解配置,擴展軟件功能的效果。這也就是設(shè)計模式的總則,開閉原則。有過軟件重構(gòu)和擴展經(jīng)驗的同學(xué)應(yīng)該對這個有比較直觀的感受。
所以我們知道,依賴關(guān)系的優(yōu)化,并不是指模型簡化,而是代碼層級的降低。而dagger2的本質(zhì)功能,就是通過注解聲明的依賴關(guān)系,幫我們生成模板類來完成這種依賴降級。下面我們來實際操作一把,看看如何使用dagger2將Car和Engine的依賴層級將為0。
使用Dagger2實現(xiàn)Engine的依賴注入
要實現(xiàn)依賴注入,首先我們要聲明一個依賴對象的需求R(engine),這個聲明就是通過@Inject注解來實現(xiàn)的,所以我們的Car類的實現(xiàn)如下:
public class Car {
String mName;
@Inject
Engine mEngine;
public Car(Engine engine){
mEngine = engine;
}
public String getName(){
return mName;
}
Engine getEngine(){
return mEngine;
}
}
聲明了需求之后,我們要聲明提供方P(engine)。提供方的聲明有兩種方式,一種是使用@Inject注解被依賴對象的構(gòu)造方法,一種是@Provide注解Module類的普通方法。我們先用第一種來實現(xiàn)。
class Engine {
public final int CYLINDER_FUEL_COST = 10;
int mCylinderNumbers;
@Inject
public Engine(){
mCylinderNumbers = 1;
}
public Engine(int cylinderNumbers){
this.mCylinderNumbers = cylinderNumbers;
}
public int getCylinderNumbers(){
return mCylinderNumbers;
}
public void run(Fuel fuel){
fuel.burn(getCylinderNumbers() * CYLINDER_FUEL_COST);
}
}
這樣我們就聲明了一個完整的依賴關(guān)系,但是在Dagger2里,我們聲明的依賴關(guān)系是無法直接使用的,我們需要使用聲明組件(Component)作為依賴關(guān)系的容器,dagger2才會根據(jù)組件里要求的依賴關(guān)系,幫我們生成組件對應(yīng)的Dagger開頭工具類。我們使用@Component來聲明一個組件。
@Component
public interface EngineComponent {
Engine getEngine();
}
這里有幾個問題點:
- 為什么組件是一個接口,而不是一個類?
- getEngine方法既不見調(diào)用方,也不見提供方的,Dagger如何綁定需求方和提供方?
這里我們先不做解答,我們先試試效果,構(gòu)建一下生成DaggerEngineComponent類,并用它為Car類創(chuàng)建Engine對象。
public class Main {
public static void main(String[] args){
Engine engine = DaggerEngineComponent.builder().build().getEngine();
Car car = new Car(engine);
System.out.println("cylinderNumbers : " + car.getEngine().getCylinderNumbers());
}
}
運行一下看到打印:
cylinderNumbers : 1
Process finished with exit code 0
說明我們的Engine對象確實是由標(biāo)記的構(gòu)造函數(shù)創(chuàng)建的。但是我們看main方法,我們通過Dagger組件創(chuàng)建了Engine對象,再手動構(gòu)造Car對象,并將engine傳遞給它,然后又取出來使用,這好像不關(guān)Car什么事啊?而且這也太麻煩了吧?
沒錯,這確實不關(guān)Car什么事,我們在Car類內(nèi)部,使用@Inject聲明一個依賴,Dagger并不認(rèn)為這個依賴只能Car來使用,同時Dagger2也不會將這個依賴自動注入到Car的內(nèi)部,需要我們手動來實現(xiàn)。Dagger2永遠只關(guān)心聲明的依賴類型、提供類型、依賴的容器類型,至于你在哪聲明,怎么注入Dagger2并不關(guān)心,也沒法關(guān)心。我看資料的時候,看到很多例子,一開始就是直接調(diào)用組件對自身進行注入的,像這樣:
XXXComponent.builder().build().inject(this);
其實很容易誤導(dǎo)我們,以為Dagger2對依賴關(guān)系的使用也進行了約束和實現(xiàn),這是不利于我們清楚地認(rèn)知dagger2的。在上面的例子中,如果把依賴聲明放到Main中,也是可以正常工作的,有興趣的同學(xué)可以自己試驗一把。
現(xiàn)在我們聲明了Engine的需求和提供,也通過Dagger2實現(xiàn)了自動創(chuàng)建Engine對象,但是每次都需要手動創(chuàng)建對象,再通過構(gòu)造方法注入到Car對象里,使用起來是很不方便的。實際上,從依賴關(guān)系的傳遞角度來看,它從原來的:
D(Car,Engin) = D(Car,Main) + D(Main,Engin)
變成了:
D(Car,Engin) = D(Car,Main) + D(Main,DaggerEngineComponent) + D(DaggerEngineComponent,Engine)
因為D(DaggerEngineComponent,Engine)是我們用注解聲明,由Dagger2生成的,它的層級等于0,所以我們可以忽略掉這部分依賴。上述的模型就成了
D(Car,Engin) = D(Car,Main) + D(Main,DaggerEngineComponent)
這兩個依賴的代碼層級都是1,也就是說我們還沒有實現(xiàn)完全的依賴托管,所以我們需要對這個依賴關(guān)系做進一步的優(yōu)化。
從代碼里我們可以看到,現(xiàn)在這個依賴的存在是為了幫我們把Engine注入到Car的對象中,所以我們下一步就要使用Dagger2來進行Engine的注入。
使用Dagger2實現(xiàn)Engine的注入
前面我們介紹了需求和提供的聲明,是通過注解來實現(xiàn)的。那要聲明一個注入,要使用什么注解呢?實際上就是同一個@Inject注解。我們在Car的內(nèi)部,使用@Inject來聲明一個需求類型的時候,實際上我們也聲明了一個注入類型,如果我們要用表達式來符號化這種類型,依賴需求類型可以寫成:
R(Car,Engin)
為了表示注入類型,我們增加一個定義:
- 注入關(guān)系I(Injector)
I(a,b,c,...)表示a需要注入b,c,...類型的對象。
代入我們例子中的類,注入需求類型可以寫成
I(Car,Engine)
上一個小節(jié)我們說了R(Car,Engine)只是聲明了Engine的需求,Dagger2生成的組件無法限制這種需求被誰使用,其實可以理解為:
D(Car,Engine) = R(? , Engine) + I(Car, Engine)
所以我們上面的例子,其實只使用了R(?,Engine)這部分定義,現(xiàn)在我們要使用I(Car,Engine)的定義。因為這個注入類型,也是依賴關(guān)系定義的一種,所以我們也需要Component作為它的容器,才能使用。我們創(chuàng)建一個CarComponent:
@Component
public interface CarComponent {
void inject(Car car);
}
這就聲明了注入類型的使用了,Dagger同樣會根據(jù)容器的聲明為我們生成Dagger開頭的組件。如果你想在EngineComponent里,通過這個方法來聲明使用這個注入類型,也是可以的。
public class Main {
public static void main(String[] args){
Car car = new Car(null);
DaggerCarComponent.builder().build().inject(car);
System.out.println("cylinderNumbers : " + car.getEngine().getCylinderNumbers());
}
}
運行結(jié)果和上面的調(diào)用方式是一樣的,這里就不貼了。發(fā)現(xiàn)沒有,這里我們沒有再通過組件創(chuàng)建Engine對象了,很神奇是不是?
是的,如果我們在組件中聲明了注入或者需求類型,Dagger2會根據(jù)聲明的類型自動查找這個類型依賴的所有的其他需求類型,并在注入組件中幫我們一一實現(xiàn)需求的創(chuàng)建(如果需求類型找不到,就會報編譯錯誤),后面我們進行原理分析的時候再進行講解,這里先記住這點。也正是因為這個自動實現(xiàn)的邏輯,讓初學(xué)者在dagger2的工作原理的理解上頗費周章,希望這樣講解過后,讀者可以理解需求類型和注入類型是相互獨立的兩個邏輯,是可以靈活使用的。
現(xiàn)在我們再來看看依賴關(guān)系的模型,它變成了:
D(Car,Engine) = D(Car,Main) + D(Main, DaggerCarComponent)
這個依賴模型的層級同樣是1,也就是說雖然我們使用Dagger進行了注入,但是這個注入過程還是手動實現(xiàn)的。有一些例子會在Car的構(gòu)造方法里,調(diào)用組件對自身進行注入,像這樣:
public class Car {
@Inject
Engine mEngine;
public Car(){
DaggerCarComponent.builder().build().inject(car);
}
}
那么在main方法里,會更加簡潔,直接new 一個 Car對象出來就可以了。這個在使用上是減少了一步,更加方便了,但是我們看到依賴模型上是這樣的:
D(Car,Engine) = D(Car,DaggerCarComponent)
只是使用DaggerCarComponent替換了Engine,依賴的代碼層級是2,也就是說如果我的CarComponent組件發(fā)生了改變,還是要回頭來修改Car的代碼,沒有解決根本問題。用依賴注入的術(shù)語來說就是,一個類不應(yīng)該了解它所依賴的對象是如何被創(chuàng)建和注入的,而這里違反了第二個原則。
下一步要怎么優(yōu)化呢?我們來分析一下,上面這個依賴模型,我們發(fā)現(xiàn) D(Car,Main) 的存在是為了幫我們創(chuàng)建Car對象, D(Car,DaggerCarComponent)的存在是為了幫我們注入Engine對象,我們分別對它倆動刀子。首先是Car的創(chuàng)建,我們在Main方法里聲明一個R(Main,Car),然后使用@Inject注解Car構(gòu)造方法,聲明Car的提供。
public class Car {
String mName;
@Inject
Engine mEngine;
@Inject
public Car(Engine engine){
mEngine = engine;
}
public String getName(){
return mName;
}
Engine getEngine(){
return mEngine;
}
}
這里我們使用了Dagger2的一個特性。因為我們的構(gòu)造方法是帶參數(shù)的,Dagger2會根據(jù)參數(shù)類型,自動創(chuàng)建需求類型,查找提供類型,在調(diào)用這個構(gòu)造方法的時候,幫我們創(chuàng)建好這個對象,并當(dāng)做參數(shù)來使用。這樣就避免了我們手動調(diào)用注入方法來初始化對象。這個特性使得Dagger2可以非常方便地幫我們管理復(fù)雜的依賴模型。
然后我們在CarComponent里聲明使用這個依賴關(guān)系。
@Component
public interface CarComponent {
void inject(Car car);
Car getCar();
}
Main方法里就成了這樣的了:
public class Main {
@Inject
Car mCar;
public static void main(String[] args){
Car car = DaggerCarComponent.builder().build().getCar();
System.out.println("cylinderNumbers : " + car.getEngine().getCylinderNumbers());
}
}
編譯構(gòu)建一下,結(jié)果當(dāng)然是正確的。現(xiàn)在我們查看一下依賴模型:
D(Car,Engine) = D(DaggerCarComponent,Engine)
后者是Dagger2托管的,層級為0,所以我們實現(xiàn)了Car和Engine的完全解耦。同時也可以看到,Car對它的成員Engine對象如何被創(chuàng)建和注入,是完全沒有感知的。
依賴注入的原理
接下來我們通過Dagger2幫我們生成的模板類,來分析一下它的工作原理。首先我們對上面一個例子做一個依賴關(guān)系的梳理:
- 我們使用@Inject注解Car類的mEngine成員,聲明了一個R(?, Engine)和一個I(Car,Engine)
- 我們使用@Inject注解Engine的構(gòu)造方法,聲明了一個P(Engine)
- 我們使用@Inject注解Main類的mCar成員,聲明了一個R(?, Car)
- 我們使用@Inject注解Car的構(gòu)造方法,聲明了一個P(Car)
- 我們在CarComponent接口中聲明了P(Car)
然后Dagger2根據(jù)P(Car),幫我們生了DaggerCarComponent,我們來看看源碼,路徑就不貼了。
public final class DaggerCarComponent implements CarComponent {
private DaggerCarComponent(Builder builder) {}
//Builder對象的靜態(tài)創(chuàng)建方法
public static Builder builder() {
return new Builder();
}
//CarComponent對象的靜態(tài)創(chuàng)建方法
public static CarComponent create() {
return new Builder().build();
}
//Car類的注入方法,因為我們在CarComponent里還聲明了inject(Car car)
@Override
public void inject(Car car) {
injectCar(car);
}
//Car類的創(chuàng)建和注入方法
@Override
public Car getCar() {
return injectCar(Car_Factory.newCar(new Engine()));
}
//Car類的實際注入方法
private Car injectCar(Car instance) {
Car_MembersInjector.injectMEngine(instance, new Engine());
return instance;
}
//Builder類,用來幫我們構(gòu)建DaggerCarComponent
public static final class Builder {
private Builder() {}
public CarComponent build() {
return new DaggerCarComponent(this);
}
}
}
我們看到Dagger提供了Buidler類來幫我們創(chuàng)建DaggerCarComponent對象,可以創(chuàng)建一個Builder也可以直接使用create的方法,非常貼心。然后我們看下getCar方法
@Override
public Car getCar() {
return injectCar(Car_Factory.newCar(new Engine()));
}
調(diào)用了一個Car_Factory.newCar方法創(chuàng)建一個新的對象,然后調(diào)用注入方法。我們看newCar方法的實現(xiàn):
public static Car newCar(Object engine) {
return new Car((Engine) engine);
}
可以看到這里就是調(diào)用的我們聲明的Car的構(gòu)造方法。這個Car_Factory就是根據(jù)第4步聲明的P(Car)生成的。因為我們Engine類里沒有其他的依賴,因此Dagger沒有幫我們生成Factory和Injector,而是直接調(diào)用@Injector標(biāo)記的構(gòu)造方法來生成Engine對象。然后我們看下injectCar方法:
private Car injectCar(Car instance) {
Car_MembersInjector.injectMEngine(instance, new Engine());
return instance;
}
inject方法調(diào)用了Car_MembersInjector. injectMEngine方法進行注入,我們來看下實現(xiàn)。
public static void injectMEngine(Car instance, Object mEngine) {
instance.mEngine = (Engine) mEngine;
}
直接賦值了。但是我們的mEngine不是public對象呀?它怎么能夠訪問呢?這是因為Dagger2利用了默認(rèn)修飾符對象的包可見性這個特性,將Car_MembersInjector類也放在了和Car類的同一個包下,只是存儲路徑不同。這也是為什么Dagger2無法注入private或者protected修飾的成員。
我們還注意到Car_MembersInjector的類聲明
public final class Car_MembersInjector implements MembersInjector<Car>
MembersInjector<Car> 又繼承于
public interface MembersInjector<T>
也就是說Dagger會為每一個I(Host,Member1,Member2,....)都生成一個實現(xiàn)了MembersInjector<Host>的,叫做Host_MembersInjector的注入類,這里的Car_MembersInjector. injectMEngine就是根據(jù)第1步聲明的I(Car,Engine)生成的。
在這個例子當(dāng)中,因為我們的邏輯非常的簡單,生成的模板也是非常的簡潔,創(chuàng)建對象,注入對象,完事。這里分析一下源碼,主要是說明一下源碼的生成邏輯,就是根據(jù)我們的依賴模型中的角色定義來實現(xiàn)的,后面復(fù)雜一點的源碼中,會對這個邏輯模型體現(xiàn)得更加完整一點。
兩個問題
還記得我們在文中提的兩個未解答的問題嗎?現(xiàn)在我們可以來回答一下了。
- 為什么組件是一個接口,而不是一個類?
組件是一個接口,是因為它聲明的依賴關(guān)系是通過注解來定義的,而這個綁定關(guān)系在組件聲明的時候是未知的,需要Dagger2來幫我們查找和實現(xiàn),所以它不能夠有方法實體。這也說明了組件不一定是一個接口,也可以是一個抽象類。
- getEngine方法既不見調(diào)用方,也不見提供方的,Dagger如何綁定需求方和提供方?
這個我們已經(jīng)解釋過了,在組件里聲明的依賴類型,Dagger都會自動幫我們查找和補全。
小結(jié)
本節(jié)我們使用了Dagger2最簡單的形式,一步一步地實現(xiàn)了Car和Engine的解耦,并建立了依賴的分析模型,通過模型來分析Dagger2的源碼,理解它的工作原理。這個模型在后續(xù)的內(nèi)容里也會使用,也是理解Dagger2工作機制的核心。接下來我們會使用Module來實現(xiàn)這個小節(jié)的例子,具體請看《Dagger2 依賴的接力游戲(三)》。
參考文檔
知乎: 神兵利器dagger2
github : Dagger2官方入口
Dagger 2 for Android Beginners
Dagger2入門解析