在 Java 的开发中,怎么利用各种设计模式和架构设计来解耦、提高拓展性,似乎已经成了一名 Java 开发人员的必修课。在一些大型系统中,还会有各种听起来高大上的名词,如 IOC、OSGI、CGLIB 等。所以这里我们就来讲讲 IOC 的一个实现框架——Dagger2。

IOC/DI

什么是 IOC(inverse of control)?

IOC 是一种面向对象编程中的一种设计原则,主要用来降低代码之间的耦合度。主要手段是通过 第三方工具 实现具有依赖关系代码之间的解耦。

IOC 名称的含义?

考虑两种情况:

  • 类 A 需要类 B 的实例,A通过 new 关键字来创建实例,这时创建和使用都是 A 做主。
  • 类 A 需要类 B 的实例,但是并没有主动使用 new 关键字,这时 A 只能被动接受外部传递给它的实例。

对比两种情况,第二种情况中 A 失去了对 B 的控制权,获取 B 的过程由主动变为被动。

什么是 DI(Dependency Injection)?

DI 就是将某个实例通过某种方式传递给需要这个实例的地方。

IOC 与 DI

  • IOC 是一种工程思想
  • DI 是一种设计模式,一种手段

Dagger2

latest version 2.13

Dagger2 是一个针对 Java 和 Android 的 静态编译时运行 的依赖注入框架。因为是编译时运行,避免了在运行时使用反射所带来的性能开销,非常适合一些性能敏感的场景。

场景分析

首先看一下初学 Java 时会写出的代码:

1
2
3
4
public class A{
private B b = new B();
public A(){}
}
1
2
3
public class B{
public B(){}
}

这里有几个问题:

  • A 对 B 的依赖程度非常高,当出现一个 NewB 的时候,需要修改 A 代码,不符合开闭原则
  • 不利于单元测试,无法使用 mock B 对象

考虑上述两个问题,可以改造成这样:

1
2
3
4
5
6
public class A{
private BaseB b;
public A(BaseB b){
this.b = b;
}
}
1
2
public abstract class BaseB{}
public class B extends BaseB{}

这样,之前的两个问题都解决了。

现在再来思考一种情况:

假如 A 是有状态的,并且很多功能模块使用到了 A,那么会有很多地方存在创建 A 实例的动作,然后因为需求不同还需要不同的 BaseB 子类。如果这时候产经突然改需求,导致我们的的 A 又需要依赖 C ,按照上一步的思想,我们可能需要给 A 构造函数添加一个参数,这时候使用到了 A 的地方就都得修改,虽然不是什么难事,但是效率(投入产出比)十分之低。

Dagger2 的可以比较完美的解决这个问题。

核心注解

先来看几个 Dagger2 最主要的四个注解。

@Inject

这个不是 Dagger2 提供的注解,而是 java 依赖注入规范提供的注解。Inject 可以用在 成员属性构造方法成员方法 上,对应于每一种情况都会有不同的作用,这里用表格来归纳下:

使用对象效果
构造器表示可以使用这个构造器来对外提供实例
成员属性表示这个属性需要被注入
方法表示这个方法在依赖注入的最后阶段会被调用

这里有几点需要注意:

  • 注入优先级: 构造器 > 成员属性 > 方法
  • 只能有一个构造器被 @Inject 标注,访问权限不能是 private
  • 成员属性 被注入的顺序不能保证
  • 方法 被调用的顺序不能保证

@Module

@Module 标注的类表明这个类会参与 依赖图 的构建,最主要的功能是 提供 依赖,提供依赖的方法(可以是静态方法)被 @Provides 标注。这些方法本身可能也需要别的依赖,这时 Dagger2 就会寻找满足这个方法的依赖,找不到就编译失败。

@Provides

配合 @Module 使用在方法上,表明这个方法会参与 依赖树 的构建。

要注意方法的访问权限

@Component

上面提到了两个代表:依赖需求方依赖供给方,但是它们之间没有直接的联系,怎么把依赖交到 依赖需求方 的手上还是个问题。
@Component 就是它们之间的桥梁,一般会使用 @Component 来标注一个 接口,然后指定 @Componentmodules 属性为定义好的 Module(依赖供给方),最后声明一个包含 依赖需求方 参数类型的函数。
这样,联系就建立好了,构建一下工程,Dagger2 的注解处理器就会帮我们生成一个 DaggerXXXX 的类,通过它我们就能完成依赖注入工作了。

实例练习

刚刚讲解完几个概念,现在来看看在它们的联系在代码中的体现:

为了便于表达,这里使用 MVP 来演示

声明 Inject

首先得确定哪些地方需要依赖注入(即依赖需求方):

1
2
3
4
5
6
public class MainActivity extends AppCompatActivity implements MainContract.MainView {
// MainActiivty 作为 MVP 中的 V,需要持有 P 层实例,即 MainContract.BaseMainPresenter 的一个实例
@Inject
protected MainContract.BaseMainPresenter presenter;
// ...
}

注意这里 presenter 的类型声明是 MainContract.BaseMainPresenter ,这是一个抽象类。
再看一下 MainContract.BaseMainPresenter 的实现类:

1
2
3
public class MainPresenter extends MainContract.BaseMainPresenter {
// ...
}

按照之前的理解,这里可以使用 @Inject 来标注 MainPresenter 的构造器,但是这里没有,而且也不行,因为 Dagger2 是静态解析的,所以我们通过 Module 来提供它的实例。

声明 Module

定义好了 依赖需求方,还差供给方没有定义:

1
2
3
4
5
6
7
@Module
public class MainModule {
@Provides
public MainContract.BaseMainPresenter provideMainPresenter() {
return new MainPresenter();
}
}

这里有几个点说明一下:

  • @Provides 标注的方法一般命名为 provideXXX
  • 返回值类型一定要和 @Inject 标注的类型一致,如 MainContract.BaseMainPresenter,否则编译失败
  • 在方法内部直接构建实例。这也是我个人比较喜欢的方式,因为这样就不需要在 MainPresenter 那边添加啥东西,配置过程都集中到 Module 里面。 再考虑另一种情况,当使用第三方库的时候,一般是没办法在它的构造器上加注解的,也只能在 Module 里面创建实例,保持风格统一总是没错的。

声明 Component

还需要 Component 来建立上面两者之间的联系:

1
2
3
4
@Component(modules = {MainModule.class})
public interface MainComponent {
void inject(MainActivity activity);
}

这个方法的名称可以随意,为了便于理解还是建议使用类似 injectXXX 的命名方式。
编译之后,Dagger2 会生成一个 DaggerMainComponent 实现 MainComponent,通过这个类就能完成注入了

完成注入

直接看代码吧:

1
2
3
4
5
6
7
8
9
public class MainActivity extends AppCompatActivity implements MainContract.MainView {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaggerMainComponent.create().inject(this);
// ...
}
}

在使用到 presenter 之前进行注入,官方说是要在 super.onCreate(savedInstanceState) 之前注入,不过还是觉得在使用之前注入合理点。

这样就完成了一个比较简单的依赖注入实践。

高级功能

除了最基本的注入之外,Dagger2 还提供了一些其他的满足我们需求的功能,比如说单例、懒加载等。

@Scope

这个用在注解上的,因为它本身没有具体定义,只是提供了一个规范。它表明一个依赖的使用规则,可以看做生命周期或者作用域(我更喜欢用生命周期表述),如我在注入 MainPresenter的时候,是注入同一个实例,还是每执行一次 inject() 就创建一个新的 MainPresenter ,这个需求还是挺常见的。

体现 Scope 的注解有:@Singleton@Resuable,下面具体说说它们的作用。

@Singleton

向 Dagger2 声明,每次都使用同一个实例进行注入。因为 Dagger2 为 Component 和 依赖
在依赖图中建立联系,所以需要保持它们两个的 Scope 是相同的,比如我在 provideMainPresenter() 上使用了 @Singleton ,那么在 MainComponent 上也要使用 @Singleton 来保持一致。
这样,通过 同一个 DaggerMainComponent 实例获取的 presenter 就都是一样的了。请注意 “同一个” 这个修饰词,如果我们连续执行 DaggerMainComponent.create().inject(this) 两次,那么每次实例还是不一样的,只有这样:

1
2
3
DaggerMainComponnet component = DaggerMainComponent.create();
component.inject(this);
component.inject(this);

注入的 presenter 才是同一个实例。
所以我们不能够指望 @Singleton 能够创建全局单例,而且它也没有处理并发的情况。

@Resuable

可重用,表示希望能够重用之前创建的实例,不过也可能还是会创建出新的实例,一般用在类声明上。不过我似乎并没看出使用和不使用有什么区别,所以也就不多嘴了。

Lazy

懒加载,只有第一次使用的时候才会真正的去创建实例。它的适用场景还是比较容易想到,比如有一些开销比较大的对象,但是又不一定会马上用到,甚至不会用上,那么只有真正用上的时候再创建是是再好不过的了。比如要懒加载 presenter,就可以这样写:

1
2
3
4
@Inject
protected Lazy<MainContract.BaseMainPresenter> presenter;
//...
presenter.get().loadMsg();

Provider

有时候可能需要很多的实例,而不是一次注入就一直是这么一个实例了。这种需求非常适合 stateful 实例,能够保证每次都能拿到一个全新的实例,不会受之前的操作影响。代码实现是这样子的:

1
2
3
@Inject Provider<MainContract.BaseMainPresenter> presenterProvider;
//...
presenterProvider.get().loadMsg();

这个例子不太恰当,不过懂大概意思就好了。

@Qualifier

Dagger2 是静态解析的,直接通过类型来进行辨别依赖关系,但是有时候光凭类型还不能确定一个依赖,那么就需要为 Dagger2 提供额外的信息,Dagger2 使用 @Named 来完成这种额外约束。下面演示一下这种需求场景的代码,还是继续改造我们的 MainPresenter

1
2
3
4
5
6
7
8
9
public class MainPresenter extends MainContract.BaseMainPresenter {
private Repo local;
private Repo remote;

public MainPresenter(Repo local, Repo remote) {
this.local = local;
this.remote = remote;
}
}

这时候 MainPresenter 多了两个 Repo 依赖,那么 MainModule 也需要做相应的更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Module
public class MainModule {
@Provides
@Reusable
public MainContract.BaseMainPresenter provideMainPresenter(
Repo local, Repo remote) {
return new MainPresenter(local, remote);
}

@Provides
public Repo provideLocalRepo() {
return new LocalRepo();
}

@Provides
public Repo provideRemoteRepo() {
return new LocalRepo();
}
}

这样看着没问题,不过 Dagger2 不认,它不知道你方法名字有啥意思,也不知道你方法内部怎么的。
所以我们需要使用 @Named 来帮助 Dagger2 判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Module
public class MainModule {
@Provides
@Reusable
public MainContract.BaseMainPresenter provideMainPresenter(
@Named("local") Repo local, @Named("remote") Repo remote) {
return new MainPresenter(local, remote);
}

@Provides
@Named("local")
public Repo provideLocalRepo() {
return new LocalRepo();
}

@Provides
@Named("remote")
public Repo provideRemoteRepo() {
return new RemoteRepo();
}
}

这样就 OK 了。

这里例子也不是很恰当,Repo 应该有一个良好的封装,Presenter 是不应该知道有 Remote 和 Local 的区别的

@BindsInstance

用在 ComponentSubComponentBuilder 的方法上,它允许我们在构建 DaggerXXXX 的时候主动提供依赖到依赖树中。还是用例子来说明,MainPresenter 能够通过 loadMsg() 来获取一条消息,但是按照常理,我们还得告诉它一个 address,不能让它 xjb 找,所以还得改改:

1
2
3
4
5
6
7
public class MainPresenter extends MainContract.BaseMainPresenter {
public MainPresenter(String address, Repo local, Repo remote) {
this.address = address;
this.local = local;
this.remote = remote;
}
}

MainModule 中再加个参数:

1
2
3
4
5
6
7
8
9
10
11
@Module
public class MainModule {
@Provides
@Reusable
public MainContract.BaseMainPresenter provideMainPresenter(
String address, @Named("local") Repo local, @Named("remote") Repo remote
) {
return new MainPresenter(address, local, remote);
}
//...
}

这样就可以了!
这样还可以把 Activity 的实例绑定上去,只要要注意下内存泄漏问题。

也可以通过手动创建 MainModule 实例来完成一些依赖的传递。

到这里,一些基本的、常用的方法就讲得差不多,更多的还是要自己踩坑,经验这东西也不好说。
Dagger2 的使用方式非常像配置文件,把依赖的构建过程从具体的业务逻辑或其它逻辑中抽离出来,使业务代码变得清晰,然后拓展性、可测试性也提高了。
如果是 Android 开发的话,可能带来的问题就是方法数增多,不过也不算大问题,毕竟还有 multidex,如果使用了热修复、插件化等技术的话,可能还需要折腾下。

完整代码在 这里

关于 Dagger2 一些更高级的用法,我们下一篇文章再交流(再不复习怕是要挂了)

参考

控制反转(IoC)与依赖注入(DI