文接《Dagger2 依赖的接力游戏(一)》
本篇代码收录在项目的chapter2分支
接下来我们要讨论依赖关系及其解决方案,在这之前我们约定几个名词和符号,在第二篇的例子当中,我们知道Car依赖Engine,这是最简单的依赖关系模型,A依赖B,我们把A叫做需求方,B叫需求对象。
所以我们约定几个名词和符号:
依赖模型的定义
- 需求方R(requirement)
- 需求对象O(object)
- 依赖关系D(dependency)
在表达式里也使用符号 -->来表示依赖方向,如果A依赖B,则可以表示为A --D(a,b)--> B,如果不强调Dab的存在,则可以简化为A-->B。
那么一个最简单的依赖模型可以表示为:
R--D(r,o)-->O
把我们例子中的类代入就得到:
R(Car)--D(Car,Engine)-->O(Engine)
如果我们在Car类的构造方法里直接创建Engine对象,这种实现就是上述的一个依赖模型的一比一还原。通过之前的讨论,我们知道D(car,engine)这种对象的依赖关系是会随着软件的规模递增,变得复杂难以维护的,所以我们需要使用依赖注入的方式来满足需求。后面我们对它进行了一个优化,在入口处动态创建Engine对象并传递给Car对象。那么在这个优化过程当中,实际上增加了一个角色:Object的提供者。所以我们增加一个定义:
- 需求提供方P(provider)
那么上述的依赖模型就变成了:
R--D(r,p)-->P--D(p,o)-->O
代入我们例子中的类:
R(Car)--D(Car,Main)-->P(Main)--D(Main,Engine)-->O(Engine)
依赖注入及优化的模型本质
我们发现依赖模型更加复杂了,是的,不管我们在编程中使用手动实现依赖注入也好,还是通过dagger2等工具实现依赖注入也好,其背后都会造成依赖模型的复杂化,这带来的一个弊端就是会导致我们的软件系统更加难以理解,逻辑更加难以跟踪,因为我们除了要理解业务逻辑之外,还要理解依赖注入的过程。
但是我们看一下上述的优化模型,其中的D(car,engin),已经被D(car,main)+D(main,engin)替换了。也就是D(r,o)=D(r,p)+D(p,o)。我们注意到main是我们程序的入口,其层级为1,而car的构造函数是我们程序的内部逻辑,其层级为2。也就是说我们通过这样一个代换,虽然增加了依赖关系的模型,但是降低了依赖层级。这种依赖层级的降低,使得软件系统结构更加地松散、灵活。如果使用dagger2等依赖注入工具,最终我们可以将依赖层级降为0,即不使用代码来维护依赖关系,而是使用注解,从而避免因依赖关系变更导致的软件系统的变更,实现基于注解配置,扩展软件功能的效果。这也就是设计模式的总则,开闭原则。有过软件重构和扩展经验的同学应该对这个有比较直观的感受。
所以我们知道,依赖关系的优化,并不是指模型简化,而是代码层级的降低。而dagger2的本质功能,就是通过注解声明的依赖关系,帮我们生成模板类来完成这种依赖降级。下面我们来实际操作一把,看看如何使用dagger2将Car和Engine的依赖层级将为0。
使用Dagger2实现Engine的依赖注入
要实现依赖注入,首先我们要声明一个依赖对象的需求R(engine),这个声明就是通过@Inject注解来实现的,所以我们的Car类的实现如下:
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注解被依赖对象的构造方法,一种是@Provide注解Module类的普通方法。我们先用第一种来实现。
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);
}
}
这样我们就声明了一个完整的依赖关系,但是在Dagger2里,我们声明的依赖关系是无法直接使用的,我们需要使用声明组件(Component)作为依赖关系的容器,dagger2才会根据组件里要求的依赖关系,帮我们生成组件对应的Dagger开头工具类。我们使用@Component来声明一个组件。
@Component
public interface EngineComponent {
Engine getEngine();
}
这里有几个问题点:
- 为什么组件是一个接口,而不是一个类?
- getEngine方法既不见调用方,也不见提供方的,Dagger如何绑定需求方和提供方?
这里我们先不做解答,我们先试试效果,构建一下生成DaggerEngineComponent类,并用它为Car类创建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对象确实是由标记的构造函数创建的。但是我们看main方法,我们通过Dagger组件创建了Engine对象,再手动构造Car对象,并将engine传递给它,然后又取出来使用,这好像不关Car什么事啊?而且这也太麻烦了吧?
没错,这确实不关Car什么事,我们在Car类内部,使用@Inject声明一个依赖,Dagger并不认为这个依赖只能Car来使用,同时Dagger2也不会将这个依赖自动注入到Car的内部,需要我们手动来实现。Dagger2永远只关心声明的依赖类型、提供类型、依赖的容器类型,至于你在哪声明,怎么注入Dagger2并不关心,也没法关心。我看资料的时候,看到很多例子,一开始就是直接调用组件对自身进行注入的,像这样:
XXXComponent.builder().build().inject(this);
其实很容易误导我们,以为Dagger2对依赖关系的使用也进行了约束和实现,这是不利于我们清楚地认知dagger2的。在上面的例子中,如果把依赖声明放到Main中,也是可以正常工作的,有兴趣的同学可以自己试验一把。
现在我们声明了Engine的需求和提供,也通过Dagger2实现了自动创建Engine对象,但是每次都需要手动创建对象,再通过构造方法注入到Car对象里,使用起来是很不方便的。实际上,从依赖关系的传递角度来看,它从原来的:
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,也就是说我们还没有实现完全的依赖托管,所以我们需要对这个依赖关系做进一步的优化。
从代码里我们可以看到,现在这个依赖的存在是为了帮我们把Engine注入到Car的对象中,所以我们下一步就要使用Dagger2来进行Engine的注入。
使用Dagger2实现Engine的注入
前面我们介绍了需求和提供的声明,是通过注解来实现的。那要声明一个注入,要使用什么注解呢?实际上就是同一个@Inject注解。我们在Car的内部,使用@Inject来声明一个需求类型的时候,实际上我们也声明了一个注入类型,如果我们要用表达式来符号化这种类型,依赖需求类型可以写成:
R(Car,Engin)
为了表示注入类型,我们增加一个定义:
- 注入关系I(Injector)
I(a,b,c,...)表示a需要注入b,c,...类型的对象。
代入我们例子中的类,注入需求类型可以写成
I(Car,Engine)
上一个小节我们说了R(Car,Engine)只是声明了Engine的需求,Dagger2生成的组件无法限制这种需求被谁使用,其实可以理解为:
D(Car,Engine) = R(? , Engine) + I(Car, Engine)
所以我们上面的例子,其实只使用了R(?,Engine)这部分定义,现在我们要使用I(Car,Engine)的定义。因为这个注入类型,也是依赖关系定义的一种,所以我们也需要Component作为它的容器,才能使用。我们创建一个CarComponent:
@Component
public interface CarComponent {
void inject(Car car);
}
这就声明了注入类型的使用了,Dagger同样会根据容器的声明为我们生成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());
}
}
运行结果和上面的调用方式是一样的,这里就不贴了。发现没有,这里我们没有再通过组件创建Engine对象了,很神奇是不是?
是的,如果我们在组件中声明了注入或者需求类型,Dagger2会根据声明的类型自动查找这个类型依赖的所有的其他需求类型,并在注入组件中帮我们一一实现需求的创建(如果需求类型找不到,就会报编译错误),后面我们进行原理分析的时候再进行讲解,这里先记住这点。也正是因为这个自动实现的逻辑,让初学者在dagger2的工作原理的理解上颇费周章,希望这样讲解过后,读者可以理解需求类型和注入类型是相互独立的两个逻辑,是可以灵活使用的。
现在我们再来看看依赖关系的模型,它变成了:
D(Car,Engine) = D(Car,Main) + D(Main, DaggerCarComponent)
这个依赖模型的层级同样是1,也就是说虽然我们使用Dagger进行了注入,但是这个注入过程还是手动实现的。有一些例子会在Car的构造方法里,调用组件对自身进行注入,像这样:
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组件发生了改变,还是要回头来修改Car的代码,没有解决根本问题。用依赖注入的术语来说就是,一个类不应该了解它所依赖的对象是如何被创建和注入的,而这里违反了第二个原则。
下一步要怎么优化呢?我们来分析一下,上面这个依赖模型,我们发现 D(Car,Main) 的存在是为了帮我们创建Car对象, D(Car,DaggerCarComponent)的存在是为了帮我们注入Engine对象,我们分别对它俩动刀子。首先是Car的创建,我们在Main方法里声明一个R(Main,Car),然后使用@Inject注解Car构造方法,声明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的一个特性。因为我们的构造方法是带参数的,Dagger2会根据参数类型,自动创建需求类型,查找提供类型,在调用这个构造方法的时候,帮我们创建好这个对象,并当做参数来使用。这样就避免了我们手动调用注入方法来初始化对象。这个特性使得Dagger2可以非常方便地帮我们管理复杂的依赖模型。
然后我们在CarComponent里声明使用这个依赖关系。
@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());
}
}
编译构建一下,结果当然是正确的。现在我们查看一下依赖模型:
D(Car,Engine) = D(DaggerCarComponent,Engine)
后者是Dagger2托管的,层级为0,所以我们实现了Car和Engine的完全解耦。同时也可以看到,Car对它的成员Engine对象如何被创建和注入,是完全没有感知的。
依赖注入的原理
接下来我们通过Dagger2帮我们生成的模板类,来分析一下它的工作原理。首先我们对上面一个例子做一个依赖关系的梳理:
- 我们使用@Inject注解Car类的mEngine成员,声明了一个R(?, Engine)和一个I(Car,Engine)
- 我们使用@Inject注解Engine的构造方法,声明了一个P(Engine)
- 我们使用@Inject注解Main类的mCar成员,声明了一个R(?, Car)
- 我们使用@Inject注解Car的构造方法,声明了一个P(Car)
- 我们在CarComponent接口中声明了P(Car)
然后Dagger2根据P(Car),帮我们生了DaggerCarComponent,我们来看看源码,路径就不贴了。
public final class DaggerCarComponent implements CarComponent {
private DaggerCarComponent(Builder builder) {}
//Builder对象的静态创建方法
public static Builder builder() {
return new Builder();
}
//CarComponent对象的静态创建方法
public static CarComponent create() {
return new Builder().build();
}
//Car类的注入方法,因为我们在CarComponent里还声明了inject(Car car)
@Override
public void inject(Car car) {
injectCar(car);
}
//Car类的创建和注入方法
@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类,用来帮我们构建DaggerCarComponent
public static final class Builder {
private Builder() {}
public CarComponent build() {
return new DaggerCarComponent(this);
}
}
}
我们看到Dagger提供了Buidler类来帮我们创建DaggerCarComponent对象,可以创建一个Builder也可以直接使用create的方法,非常贴心。然后我们看下getCar方法
@Override
public Car getCar() {
return injectCar(Car_Factory.newCar(new Engine()));
}
调用了一个Car_Factory.newCar方法创建一个新的对象,然后调用注入方法。我们看newCar方法的实现:
public static Car newCar(Object engine) {
return new Car((Engine) engine);
}
可以看到这里就是调用的我们声明的Car的构造方法。这个Car_Factory就是根据第4步声明的P(Car)生成的。因为我们Engine类里没有其他的依赖,因此Dagger没有帮我们生成Factory和Injector,而是直接调用@Injector标记的构造方法来生成Engine对象。然后我们看下injectCar方法:
private Car injectCar(Car instance) {
Car_MembersInjector.injectMEngine(instance, new Engine());
return instance;
}
inject方法调用了Car_MembersInjector. injectMEngine方法进行注入,我们来看下实现。
public static void injectMEngine(Car instance, Object mEngine) {
instance.mEngine = (Engine) mEngine;
}
直接赋值了。但是我们的mEngine不是public对象呀?它怎么能够访问呢?这是因为Dagger2利用了默认修饰符对象的包可见性这个特性,将Car_MembersInjector类也放在了和Car类的同一个包下,只是存储路径不同。这也是为什么Dagger2无法注入private或者protected修饰的成员。
我们还注意到Car_MembersInjector的类声明
public final class Car_MembersInjector implements MembersInjector
MembersInjector
public interface MembersInjector
也就是说Dagger会为每一个I(Host,Member1,Member2,....)都生成一个实现了MembersInjector
在这个例子当中,因为我们的逻辑非常的简单,生成的模板也是非常的简洁,创建对象,注入对象,完事。这里分析一下源码,主要是说明一下源码的生成逻辑,就是根据我们的依赖模型中的角色定义来实现的,后面复杂一点的源码中,会对这个逻辑模型体现得更加完整一点。
两个问题
还记得我们在文中提的两个未解答的问题吗?现在我们可以来回答一下了。
- 为什么组件是一个接口,而不是一个类?
组件是一个接口,是因为它声明的依赖关系是通过注解来定义的,而这个绑定关系在组件声明的时候是未知的,需要Dagger2来帮我们查找和实现,所以它不能够有方法实体。这也说明了组件不一定是一个接口,也可以是一个抽象类。
- getEngine方法既不见调用方,也不见提供方的,Dagger如何绑定需求方和提供方?
这个我们已经解释过了,在组件里声明的依赖类型,Dagger都会自动帮我们查找和补全。
小结
本节我们使用了Dagger2最简单的形式,一步一步地实现了Car和Engine的解耦,并建立了依赖的分析模型,通过模型来分析Dagger2的源码,理解它的工作原理。这个模型在后续的内容里也会使用,也是理解Dagger2工作机制的核心。接下来我们会使用Module来实现这个小节的例子,具体请看《Dagger2 依赖的接力游戏(三)》。
参考文档
知乎: 神兵利器dagger2
github : Dagger2官方入口
Dagger 2 for Android Beginners
Dagger2入门解析