一、什么叫依赖和依赖倒置
依赖是类与类之间的连接,依赖关系表示一个类依赖于另一个类的定义,通俗来讲就是一种需要。
依赖倒置是面向对象设计领域的一种软件设计原则。这里就要逐一讲一下,软件设计的 6 大设计原则,合称 SOLID
1、单一职责原则(Single Responsibility Principle,简称SRP )
- 核心思想: 应该有且仅有一个原因引起类的变更,啥意思呢?就是说一个类或者模块只初一件或一类任务。
- 问题描述: 假如有类Class1完成职责T1,T2,当职责T1或T2有变更需要修改时,有可能影响到该类的另外一个职责正常工作。
- 好处: 类的复杂度降低、可读性提高、可维护性提高、扩展性提高、降低了变更引起的风险。
- 需注意: 单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可以度量的,因项目和环境而异。
2、里氏替换原则(Liskov Substitution Principle,简称LSP)
- 核心思想: 在使用基类的的地方可以任意使用其子类,能保证子类完美替换基类,即使用父类的地方替换成子类不会有任何问题。
- 通俗来讲: 只要父类能出现的地方子类就能出现。反之,父类则未必能胜任。
- 好处: 增强程序的健壮性,即使增加了子类,原有的子类还可以继续运行。
- 需注意: 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系 采用依赖、聚合、组合等关系代替继承。
3、依赖倒置原则(Dependence Inversion Principle,简称DIP)
- 核心思想:高层模块不应该依赖底层模块,二者都该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象;
- 说明:高层模块就是调用端,低层模块就是具体实现类。抽象就是指接口或抽象类。细节就是实现类。
- 通俗来讲: 依赖倒置原则的本质就是通过抽象(接口或抽象类)使个各类或模块的实现彼此独立,互不影响,实现模块间的松耦合。
- 问题描述: 类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
- 解决方案: 将类A修改为依赖接口interface,类B和类C各自实现接口interface,类A通过接口interface间接与类B或者类C发生联系,则会大大降低修改类A的几率。
- 好处:依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中可以减少需求变化引起的工作量。使并行开发更友好。
4、接口隔离原则(Interface Segregation Principle,简称ISP)
- 核心思想:类间的依赖关系应该建立在最小的接口上
- 通俗来讲: 建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
- 问题描述: 类A通过接口interface依赖类B,类C通过接口interface依赖类D,如果接口interface对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
- 需注意:
- 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情
- 为依赖接口的类定制服务。只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
5、迪米特法则(Law of Demeter,简称LoD)
- 核心思想: 类间解耦。
- 通俗来讲: 一个类对自己依赖的类知道的越少越好。自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。
6、开放封闭原则(Open Close Principle,简称OCP)
- 核心思想: 尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化
- 通俗来讲: 一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。
依赖倒置原则的定义如下:
- 上层模块不应该依赖底层模块,它们都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
二、什么是上层模块和底层模块?
不管你承认不承认,“有人的地方就有江湖”,我们都说人人平等,但是对于任何一个组织机构而言,它一定有架构的设计有职能的划分。按照职能的重要性,自然而然就有了上下之分。并且,随着模块的粒度划分不同这种上层与底层模块会进行变动,也许某一模块相对于另外一模块它是底层,但是相对于其他模块它又可能是上层
公司管理层就是上层,CEO 是整个事业群的上层,那么 CEO 职能之下就是底层。
然后,我们再以事业群为整个体系划分模块,各个部门经理以上部分是上层,那么之下的组织都可以称为底层。
由此,我们可以看到,在一个特定体系中,上层模块与底层模块可以按照决策能力高低为准绳进行划分。
那么,映射到我们软件实际开发中,一般我们也会将软件进行模块划分,比如业务层、逻辑层和数据层。
业务层中是软件真正要进行的操作,也就是做什么。 逻辑层是软件现阶段为了业务层的需求提供的实现细节,也就是怎么做。 数据层指业务层和逻辑层所需要的数据模型。
因此,如前面所总结,按照决策能力的高低进行模块划分。业务层自然就处于上层模块,逻辑层和数据层自然就归类为底层。
三、什么是抽象和细节?
抽象如其名字一样,是一件很抽象的事物。抽象往往是相对于具体而言的,具体也可以被称为细节,当然也被称为具象。
比如:
- 这是一幅画。画是抽象,而油画、素描、国画而言就是具体。
- 这是一件艺术品,艺术品是抽象,而画、照片、瓷器等等就是具体了。
- 交通工具是抽象,而公交车、单车、火车等就是具体了。
- 表演是抽象,而唱歌、跳舞、小品等就是具体。
上面可以知道,抽象可以是物也可以是行为。具体映射到软件开发中,抽象可以是接口或者抽象类形式。
/**
* Driveable 是接口,所以它是抽象
*/
public interface Driveable {
void drive();
}
/**
* 而 Bike 实现了接口,它们被称为具体。
*/
public class Bike implements Driveable {
@Override
public void drive() {
System.out.println("Bike drive");
}
}
/**
* 而 Car实现了接口,它们被称为具体。
*/
public class Car implements Driveable {
@Override
public void drive() {
System.out.println("Car drive.");
}
}
四、依赖倒置的好处
在平常的开发中,我们大概都会这样编码。
public class Person {
private Bike mBike;
private Car mCar;
private Train mTrain;
public Person(){
mBike = new Bike();
//mCar = new Car();
// mTrain = new Train();
}
public void goOut(){
System.out.println("出门啦");
mBike.drive();
//mCar.drive();
// mTrain.drive();
}
public static void main(String ... args){
//TODO:
Person person = new Person();
person.goOut();
}
}
我们创建了一个 Person 类,它拥有一台自行车,出门的时候就骑自行车。
不过,自行车适应很短的距离。如果,我要出门逛街呢?自行车就不大合适了。于是就要改成汽车。
不过,如果我要到北京去,那么汽车也不合适了。
有没有一种方法能让 Person 的变动少一点呢?因为这是最基础的演示代码,如果工程大了,代码复杂了,Person 面对需求变动时改动的地方会更多。
而依赖倒置原则正好适用于解决这类情况。
下面,我们尝试运用依赖倒置原则对代码进行改造。我们再次回顾下它的定义:
上层模块不应该依赖底层模块,它们都应该依赖于抽象。 抽象不应该依赖于细节,细节应该依赖于抽象。 首先是上层模块和底层模块的拆分。
按照决策能力高低或者重要性划分,Person 属于上层模块,Bike、Car 和 Train 属于底层模块。
上层模块不应该依赖于底层模块。
public class Person {
private Driveable mDriveable;
public Person(){
mDriveable = new Train();
}
public void goOut(){
System.out.println("出门啦");
mDriveable.drive();
}
public static void main(String ... args){
//TODO:
Person person = new Person();
person.goOut();
}
}
可以看到,依赖倒置实质上是面向接口编程的体现。
五、Dagger2注解
Dagger2是基于Java注解来实现依赖注入的,那么在正式使用之前我们需要先了解下Dagger2中的注解Dagger2使用过程中我们通常接触到的注解主要包括:@Inject, @Module, @Provides, @Component, @Qulifier, @Scope, @Singleten。
- @Inject:@Inject有两个作用,一是用来标记需要依赖的变量,以此告诉Dagger2为它提供依赖;二是用来标记构造函数,Dagger2通过@Inject注解可以在需要这个类实例的时候来找到这个构造函数并把相关实例构造出来,以此来为被@Inject标记了的变量提供依赖;注意:被@Inject注解的变量或构造函数不能够被private或public这些修饰符修饰。
- @Module:@Module用于标注提供依赖的类。你可能会有点困惑,上面不是提到用@Inject标记构造函数就可以提供依赖了么,为什么还需要@Module?很多时候我们需要提供依赖的构造函数是第三方库的,我们没法给它加上@Inject注解,又比如说提供以来的构造函数是带参数的,如果我们之所简单的使用@Inject标记它,那么他的参数又怎么来呢?@Module正是帮我们解决这些问题的。
- @Provides:@Provides用于标注Module所标注的类中的方法,该方法在需要提供依赖时被调用,从而把预先提供好的对象当做依赖给标注了@Inject的变量赋值;注意:被@Provides注解的变量或构造函数不能够被private或public这些修饰符修饰。
- @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-@Inject来提供依赖
Car类是需求依赖方,依赖了Engine类;因此我们需要在类变量Engine上添加@Inject来告诉Dagger2来为自己提供依赖。 Engine类是依赖提供方,因此我们需要在它的构造函数上添加@Inject
public class Engine {
/**
* 这里的@Inject用来标记构造函数,Dagger2通过@Inject注解可以在需要这个类实例的时候来找到这个构造函数并把相关实例构造出来,
* 以此来为被@Inject标记了的变量提供依赖
* 需要注意的是:被@Inject标记的变量或者构造函数都不能够被private或public等修饰符修饰,否则会编译失败(apt生成的java文件无法编译成功)
*/
@Inject
Engine(){}
@Override
public String toString() {
return "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 class Car {
/**
* @Inject:@Inject有两个作用,一是用来标记需要依赖的变量,以此告诉Dagger2为它提供依赖
*/
@Inject
Engine engine;
public Car() {
DaggerCarComponent.builder().build().inject(this);
}
public Engine getEngine() {
return this.engine;
}
public static void main(String ... args){
//TODO:
Car car = new Car();
System.out.println(car.getEngine());
}
}
2、案例B-@Module来提供依赖
如果创建Engine的构造函数是带参数的呢?比如说制造一台引擎是需要齿轮(Gear)的。或者Eggine类是我们无法修改的呢?这时候就需要@Module和@Provide上场了。
同样我们需要在Car类的成员变量Engine上加上@Inject表示自己需要Dagger2为自己提供依赖;Engine类的无参构造函数上的@Inject可以去掉也可以不去掉,因为现在不需要通过构造函数上的@Inject来提供依赖了,而是优先通过Module来提供依赖了。
public class Engine {
private String name;
@Inject
Engine(){}
Engine(String name) {
this.name = name;
}
@Override
public String toString() {
return "Engine{" +
"name='" + name + '\'' +
'}';
}
public void run() {
System.out.println("引擎转起来了~~~");
}
}
接着我们需要一个Module类来生成依赖对象。前面介绍的@Module就是用来标准这个类的,而@Provide则是用来标注具体提供依赖对象的方法(这里有个不成文的规定,被@Provide标注的方法命名我们一般以provide开头,这并不是强制的但有益于提升代码的可读性)。
@Module
public class MarkCarModule {
public MarkCarModule(){ }
/**
* 用于标注Module所标注的类中的方法,该方法在需要提供依赖时被调用,从而把预先提供好的对象当做依赖给标注了@Inject的变量赋值
* 需要注意的是:被@Provides标记的依赖提供方法不能够被private或public等修饰符修饰,否则会编译失败(apt生成的java文件无法编译成功)
* @return
*/
@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 class Car {
/**
* 我们提到@Inject和@Module都可以提供依赖,那如果我们即在构造函数上通过标记@Inject提供依赖,有通过@Module提供依赖Dagger2会如何选择呢?具体规则如下:
*
* 步骤1:首先查找@Module标注的类中是否存在提供依赖的方法。
* 步骤2:若存在提供依赖的方法,查看该方法是否存在参数。
* a:若存在参数,则按从步骤1开始依次初始化每个参数;
* b:若不存在,则直接初始化该类实例,完成一次依赖注入。
*
*
* 步骤3:若不存在提供依赖的方法,则查找@Inject标注的构造函数,看构造函数是否存在参数。
* a:若存在参数,则从步骤1开始依次初始化每一个参数
* b:若不存在,则直接初始化该类实例,完成一次依赖注入
*/
@Inject
Engine engine;
public Car() {
//这里需要注意的是:加不加markCarModule(new MarkCarModule())最后执行的结果都不一样的。因为dagger2会根据我们上面提到的规则,优先由Module来提供依赖。所以下面两种写法执行结果都是相同的。
DaggerCarComponent.builder().markCarModule(new MarkCarModule()) .build().inject(this);
// DaggerCarComponent.builder().build().inject(this);
}
public Engine getEngine() {
return this.engine;
}
public static void main(String ... args){
//TODO:
Car car = new Car();
System.out.println(car.getEngine());
}
}
这样一个最最基本的依赖注入就完成了。
而这里除了可以通过modules指定依赖,还可以通过dependencies来指定modules的依赖,这句话什么意思呢?。
一般我们用Dagger2 的时候,都是在Application中生成一个AppComponent,然后其他的功能模块的Component依赖于AppComponent,但是我们使用的方式可能会有所不同,在BeggarComponent上有时会用@Component(modules = BeggarModule.class, dependencies = AppComponent.class)
,也有使用@Subcomponent(modules = BeggarModule.class)
方式提供Component,为了更好的理解Component和SubComponent,这里假设AppComponent是有权有势的地主,BeggarComponent比如为需要食物和住宿的流浪者,结合代码进行理解,代码如下:
一、Module模块
- AppModule.java
@Module
public class AppModule { //地主的后勤部门
@Provides
public Food provideFood() { //后勤提供食物
return new Food();
}
@Provides
public Accommodation provideAccommodation() { //后勤提供住宿
return new Accommodation();
}
@Provides
public Salary provideSalary() { //提供工钱
return new Salary();
}
}
- BeggarModule.java
@Module
public class BeggarModule {
@Provides
public Employee provideEmployee(Food food, Accommodation accommodation) {
// Employee 依赖 food、accommodation,
//这里的food、accommodation实例将通过dependencies 来指定AppComponent来提供依赖
return new Employee(food, accommodation);
}
}
此处的Food、Accommodation、Salary、Employee就是普通的实体类。
二、@Component 方式
- AppComponent.java
@Component(modules = AppModule.class)
public interface AppComponent {
//将AppModule中的Food、Accommodation暴露出来,以便于其他依赖于AppComponent的Component调用,也就是让后勤部门把食物和住宿提供出来
Food getFood();
Accommodation getAccommodation();
}
- BeggarComponent.java
//dependencies 指定AppComponent为BeggarModule提供依赖
@Component(modules = BeggarModule.class, dependencies = AppComponent.class)
public interface BeggarComponent {
void inject(Mansion mansion); //去刘府报名去
}
使用方法:
public class Mansion {
@Inject
Food food;
@Inject
Accommodation accommodation;
@Inject
Employee employee;
//无法编译通过,因为Salary 在AppComponent 中没有显示的,也就是说虽然财务部门准备了工钱,可是地主没说要提供出去,也就是在AppComponent没把工钱暴露出去
//@Inject
//Salary salary;
public static void main(String[] args) {
AppModule appModule = new AppModule();
AppComponent appComponent = DaggerAppComponent.builder()
.AppModule(appModule)
.build();
BeggarModule beggarModule = new BeggarModule();
BeggarComponent beggarComponent = DaggerBeggarComponent.builder()
.appComponent(appComponent)
.beggarModule(beggarModule)
.inject(this);
System.out.println("food is -->" + food);
System.out.println("accommodation is -->" + accommodation);
System.out.println("employee is -->" + employee);
}
}
3、案例C
那么如果一台汽车有两个引擎(也就是说Car类中有两个Engine变量)怎么办呢?没关系,我们还有@Qulifier!首先我们需要使用Qulifier定义两个注解:
public class Engine {
/**
* 用于自定义注解,也就是说@Qulifier就如同Java提供的几种基本元注解一样用来标记注解类。我们在使用@Module来标注提供依赖的方法时,方法名我们是可以随便定义的(虽然我们定义方法名一般以provide开头,但这并不是强制的,只是为了增加可读性而已)。那么Dagger2怎么知道这个方法是为谁提供依赖呢?答案就是返回值的类型,Dagger2根据返回值的类型来决定为哪个被@Inject标记了的变量赋值。但是问题来了,一旦有多个一样的返回类型Dagger2就懵逼了。@Qulifier的存在正式为了解决这个问题,我们使用@Qulifier来定义自己的注解,然后通过自定义的注解去标注提供依赖的方法和依赖需求方(也就是被@Inject标注的变量),这样Dagger2就知道为谁提供依赖了。----一个更为精简的定义:当类型不足以鉴别一个依赖的时候,我们就可以使用这个注解标示
* 1. 使用@Qulifier定义两个注解
*/
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QualifierA { }
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QualifierB { }
private String name;
Engine(String name) {
this.name = name;
}
@Override
public String toString() {
return "Engine{" +
"name='" + name + '\'' +
'}';
}
public void run() {
System.out.println("引擎转起来了~~~");
}
}
同时我们需要对依赖提供方Module做出修改
@Module
public class MarkCarModule {
public MarkCarModule(){ }
/**
* 2. 同时我们需要对依赖提供方做出修改
* @return
*/
@Engine.QualifierA
@Provides
Engine provideEngineA(){
return new Engine("gearA");
}
@Engine.QualifierB
@Provides
Engine provideEngineB(){
return new Engine("gearB");
}
}
接下来依赖需求方Car类同样需要修改
public class Car {
/**
* 3. 接下来依赖需求方Car类同样需要修改
*/
@Engine.QualifierA
@Inject
Engine engineA;
@Engine.QualifierB
@Inject
Engine engineB;
public Car() {
DaggerCarComponent.builder().markCarModule(new MarkCarModule())
.build().inject(this);
}
public Engine getEngineA() {
return this.engineA;
}
public Engine getEngineB() {
return this.engineB;
}
public static void main(String... args) {
//TODO:
Car car = new Car();
System.out.println(car.getEngineA());
System.out.println(car.getEngineB());
}
}
执行结果:
Engine{name='gearA'}
Engine{name='gearB'}
但是看上面的代码发现好像要自定义一个注解,有点烦这。所以这种情况dagger2已经为我们提供了一个写好的注解@Name,而这个@Name就是被 @Qualifier注解过的自定义注解。所以上面这个案例还可以这样来写,Engine类不需要改动,只需要改动Module和Car即可
@Module
public class MarkCarModule {
public MarkCarModule(){ }
/**
* 1. 我们需要对依赖提供方做出修改,这里的@Name("A")括号里面的value值要与依赖需求方的value值保持一致。
* @return
*/
@Name("A")
@Provides
Engine provideEngineA(){
return new Engine("gearA");
}
@Name("B")
@Provides
Engine provideEngineB(){
return new Engine("gearB");
}
}
public class Car {
/**
* 2. 接下来依赖需求方Car类同样需要修改
*/
@Name("A")
@Inject
Engine engineA;
@Name("B")
@Inject
Engine engineB;
public Car() {
DaggerCarComponent.builder().markCarModule(new MarkCarModule())
.build().inject(this);
}
public Engine getEngineA() {
return this.engineA;
}
public Engine getEngineB() {
return this.engineB;
}
public static void main(String... args) {
//TODO:
Car car = new Car();
System.out.println(car.getEngineA());
System.out.println(car.getEngineB());
}
}
最终执行结果与上面一致。