Dagger2 依赖的接力游戏(二):依赖模型的建立和实现

文接《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();

}

这里有几个问题点:

  1. 为什么组件是一个接口,而不是一个类?
  2. 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帮我们生成的模板类,来分析一下它的工作原理。首先我们对上面一个例子做一个依赖关系的梳理:

  1. 我们使用@Inject注解Car类的mEngine成员,声明了一个R(?, Engine)和一个I(Car,Engine)
  2. 我们使用@Inject注解Engine的构造方法,声明了一个P(Engine)
  3. 我们使用@Inject注解Main类的mCar成员,声明了一个R(?, Car)
  4. 我们使用@Inject注解Car的构造方法,声明了一个P(Car)
  5. 我们在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的,叫做Host_MembersInjector的注入类,这里的Car_MembersInjector. injectMEngine就是根据第1步声明的I(Car,Engine)生成的。

在这个例子当中,因为我们的逻辑非常的简单,生成的模板也是非常的简洁,创建对象,注入对象,完事。这里分析一下源码,主要是说明一下源码的生成逻辑,就是根据我们的依赖模型中的角色定义来实现的,后面复杂一点的源码中,会对这个逻辑模型体现得更加完整一点。

两个问题

还记得我们在文中提的两个未解答的问题吗?现在我们可以来回答一下了。

  1. 为什么组件是一个接口,而不是一个类?

组件是一个接口,是因为它声明的依赖关系是通过注解来定义的,而这个绑定关系在组件声明的时候是未知的,需要Dagger2来帮我们查找和实现,所以它不能够有方法实体。这也说明了组件不一定是一个接口,也可以是一个抽象类。

  1. getEngine方法既不见调用方,也不见提供方的,Dagger如何绑定需求方和提供方?

这个我们已经解释过了,在组件里声明的依赖类型,Dagger都会自动帮我们查找和补全。

小结

本节我们使用了Dagger2最简单的形式,一步一步地实现了Car和Engine的解耦,并建立了依赖的分析模型,通过模型来分析Dagger2的源码,理解它的工作原理。这个模型在后续的内容里也会使用,也是理解Dagger2工作机制的核心。接下来我们会使用Module来实现这个小节的例子,具体请看《Dagger2 依赖的接力游戏(三)》。

参考文档

知乎: 神兵利器dagger2
github : Dagger2官方入口
Dagger 2 for Android Beginners
Dagger2入门解析

你可能感兴趣的:(Dagger2 依赖的接力游戏(二):依赖模型的建立和实现)