Dagger2 依赖的接力游戏(一): 什么是依赖和依赖注入?
Dagger2是用于软件解耦的依赖注入工具,由谷歌自dagger优化而来。在诸多的android框架中,属于相对复杂的一个,起码我自己在学习的过程当中,抓掉了不少的头发。虽然市面上有不少的介绍文章,浅显的、深入的都有,但是总感觉一次性讲齐全的还是比较少,需要东拼西凑看一大堆东西才能慢慢搭出一个框架来,还是非常痛苦的。所以在这里整理一下自己的理解,希望能够帮助到一些正在抓头的同学。当然要有最直观的理解,不管看了什么资料,还是要自己动手写点例子,按照自己的理解试试对错,然后再研究一下模板代码才行。
谁适合阅读本文
本文主要目的还是为Dagger2的入门同学提供一些帮助,将尝试从依赖关系传递的角度,从依赖的概念开始,解释到Dagger2的解决方案以及实现原理。如果你是dagger2的新手,那么阅读此文应该会有所收获,如果你已经对dagger2比较熟悉或者有深入的理解,也可以尝试着从本文的角度去理解dagger2的概念,看看是否有新的收获。最后欢迎大家讨论交流,如果文中有不足或者错误,希望大家能够予以指正。
写在前面
本文不讨论如何在android平台上应用dagger2,所有的示例都直接在GoogleSample的项目基础上直接书写,并且放到了github上面,代码分支与文章章节对应。本篇的代码放在项目的chapter1分支。
Dagger2简介
dagger2是用于软件解耦的依赖注入工具,由谷歌自dagger优化而来。dagger的开发者就是大名鼎鼎的square公司,旗下还有retrofit,okhttp,greendao等等知名的开发框架。
dagger2使用注解和apt工具来生成注入依赖的模板代码,因此不会损耗运行时性能,适用于安卓平台。
dagger2使用生成模板代码的方式来注入依赖,使得依赖关系的错误,在编译阶段就能发现,并且模板代码相对简洁规范,可读性强,便于理解。
学习dagger2的关键还是要理解它产生的需求背景,它所要解决的问题,即通过依赖注入的方式,成为软件依赖关系的代理,从而实现软件架构的解耦。其相对复杂的注解功能以及生成的模板类,其实已经是解决依赖注入过程中存在的各种类型问题的一个最简洁方案了。接下来我们将从最简单的依赖问题,向复杂的依赖问题展开,梳理dagger2解决这些问题的方案以及实现原理,以及这个方案中依赖关系传递过程。
什么是依赖?
在OOP(面向对象编程)中,一个类需要另一个类的配合才能进行工作的情况,叫做依赖。依赖根据需求方和提供方的作用关系,又分为类依赖、成员依赖、方法依赖。
比如我们有一个汽车的类,它有名字和引擎。
public class Car {
String mName;
Engine mEngine;
public String getName(){
return mName;
}
Engine getEngine(){
return mEngine;
}
}
汽车有引擎,引擎有自己的气缸数目。
class Engine {
public final int CYLINDER_FUEL_COST = 10;
int mCylinderNumbers;
public Engine(int cylinderNumbers){
mCylinderNumbers = cylinderNumbers;
}
public int getCylinderNumbers(){
return mCylinderNumbers;
}
public void run(Fuel fuel){
fuel.burn(getCylinderNumbers() * CYLINDER_FUEL_COST);
}
}
引擎的工作需要燃料
public class Fuel {
int mEnergy;
public void burn(int energy){
mEnergy -= energy;
}
}
我们还有一辆具体的汽车类型,蒙迪欧:
public class Mondeo extends Car {
public Mondeo(){
mName = "mondeo";
mEngine = new Engine(4);
}
}
在上述的一个简单的依赖模型中,Mondeo和Car存在继承关系,属于类依赖,类依赖属于直接依赖关系,会固化软件的依赖模型,这种依赖是在代码逻辑里写死的,没有修改代码以外的方法可以地改变这种依赖关系。如果依赖关系发生了变化,一定会引起类的变更,从而导致代码的改动。所以在OOP设计原则中,要严格控制继承关系,尽量使用组合的方式来复用逻辑。
Engine和Fuel属于方法依赖,方法依赖属于间接依赖,它所使用的对象在外部创建,调用方只关系对象的功能。因为创建逻辑在外部,因此对象可以被灵活地创建、初始化,应用多态等OOP方法,可以最大程度地避免业务逻辑的变更,导致软件系统的变更。
Car和Engine属于成员依赖,成员依赖是一个待定类型的依赖,如果一个类直接通过new的方式,初始化它的成员,比如我们的Mondeo,那么就会产生直接依赖,如果通过构造方法传递进来,那么就变成了方法依赖。
什么是依赖注入?
我们在Mondeo 类的内部直接创建了一个4缸引擎,这就产生了一个直接依赖,假设蒙迪欧有一天要升级,换成八缸的引擎,我们就不得不回来修改构造方法。如果我们优化一下:
public class Mondeo extends Car {
public Mondeo(Engine engine){
mName = "mondeo";
mEngine = engine;
}
}
public class Main {
public static void main(String[] args){
Engine engine = new Engine(4);
Car car = new Mondeo(engine);
}
}
这样引擎对象的创建过程就被放在了Car类的外部,我们对引擎的直接依赖,改为构造方法的间接依赖,这种将类的依赖需求,通过外部逻辑满足的方式,就叫做依赖注入。
(直接)依赖会产生什么问题,为什么需要依赖注入?
- 假如被依赖的类发生修改,则所有的需求方也需要同步修改,并且这种修改还伴随着传递可能性,在复杂的软件当中,会导致牵一发而动全身的结果,使软件难以扩展、维护。
比如,假如我们的引擎有不同的品牌,引擎又有不同类型的气缸,气缸又有不同类型的轴承,假如这一个依赖关系链通过直接依赖的方式来实现,如果现在增加了一种轴承的类型,导致增加一系列的配件型号,那么我们需要修改依赖的每个环节的具体类型。而这种情况在真实的软件系统中会更加严重。
- 假如一个类直接构造另一个类的对象,则这个类和另外一个类就形成了强制的绑定关系,无法独立地进行测试,降低代码的可测试性。
假如我们要统计汽车的油耗是否稳定,如果我们直接依赖了具体的引擎类型,那么如果出了问题,我们就无法得知是引擎的油耗问题,还是汽车的统计逻辑问题。如果我们采用注入依赖的方式,我们就可以通过一个Mock的引擎对象,替换真实的引擎对象,我们能够控制这个mock的对象,总是以稳定的油耗工作,从而剥离引擎问题对汽车油耗统计逻辑的影响。
小结
在本篇中,我们通过例子解释了几种依赖关系以及他们的依赖类型,解释了直接依赖会导致的问题,以及什么是依赖注入,为什么需要依赖注入。接下来我们会正式开始依赖关系的由简入繁,讲述这个过程中会出现的问题,对应的解决方案,以及原理。具体请看:
参考文档
知乎: 神兵利器dagger2
github : Dagger2官方入口
Dagger 2 for Android Beginners
Dagger2入门解析