回想起2009年我刚开始开发 Android 应用程序时,情况与现在大不相同。应用程序完全是软件开发的新领域,一切都还在发展中,没有人把它们当成一回事,它们也大多索然无味。但是,快进到今天,移动应用程序的局面已经焕然一新。
它们现在是一项大产业,并且正逐步成为很多公司战略的基石。JUST EAT 当然也不例外。我们很看重客户对 app 的满意度,也以此为激励努力开发具有健壮性和超凡用户体验的 app。
那么你可能会说,我们重视 app 的开发,是一群使用严谨软件工具和技术的严谨开发者。我们在 JUST EAT 上使用的众多软件技术其中一种就是依赖注入以及相关框架。
这只是一种设计模式
DI(依赖注入)设计模式已经不是什么新鲜技术了,但是最近频繁在 Android 开发中用到,主要是因为它提供了一些很棒的框架。
DI 让开发者能够编写低耦合代码,更容易测试。越是复杂的Android 软件开发周期越长,测试效率也就越重要。我们认为,DI是JUST EAT 代码可配置和可测试的关键,通过DI可以创建一个值得信任的代码库。
尽管我们的代码库相当庞大和复杂,但使用 DI 在一定程度上让我们具备了健壮的测试,从而保证了可以定期迅速地发布。说了这么多,我真心希望你已经相信 DI 值得一看。那么我们首先来快速了解一下 DI 吧。
依赖注入基本概念
编写代码时我们常常会发现有一些类是依赖于其它类的。所以类A可能需要一个类B的引用或对象。为了理解得更清晰些,我们来看看这个需要使用 Engine 类的 Car 类例子。
1 2 3 4 5 6 7 8 |
public class Car { private Engine engine; public Car() { engine = new PetrolEngine(); } } |
这个代码运行正常,但缺点是 Car 和 Engine 之间高度耦合。
Car 类自己创建新的 Engine 对象,则它必须准确知道 Engine 的实现方法,在这个例子中就需要 PetrolEngine 。
我们做些小的改进减少耦合度,来看看创建 Car 类的不同方法吧。
1 2 3 4 5 6 7 8 |
public class Car { private Engine engine; public Car(Engine engine) { this.engine = engine; } } |
这里,我们通过Car 的构造函数,向Car 传递了一个Engine 对象。
这意味着两个对象之间的耦合变低了。
Car类不需要知道 Engine 的具体实现,只要继承了原始 Engine 类,任何类型 Engine 都符合要求。
在这个例子中,通过 Car 类的构造函数传递,或者说是注入了依赖。其实我们已经完成了一种称为构造注入的注入方法,当然,我们也可以通过方法注入或是借助 DI 框架直接执行变量注入。
其实,DI仅仅如此。它最基本的用法就是向类中传递一个依赖,而不是直接在类中实例化。
如果依赖注入那么简单,我们为何需要框架呢?
现在我们理解了DI ,就直接在代码中使用它吧。
我们可以简单地通过一个构造器或是调用方法传递需要的依赖。这种做法对于简单的依赖来说是可行的,但是你很快会发现操作复杂依赖时会变得一团糟。
回到那个有 Engine 依赖的 Car 的例子,想象一下,要是 engine 也设置了依赖,它由曲轴、活塞、机组、头部组成。
如果我们遵循 DI 原理,就需要向 Engine 传递依赖。这情况还不是很糟,我们只需要先创建个对象,然后传递到 Engine 中,最后再传递到 Car 中。
接下来我们举个稍微复杂的例子。
想象一下,如果我们尝试着为 engine 每个部分都创建类,可以预料到最终将会得到许许多多类,有着复杂的树状(更确切是图状)结构的依赖。
例子的简化依赖关系图。必须首先创建叶子结点上的依赖然后再传递给依赖于它们的对象。所有的对象必须依次创建。
我们必须仔细地按照正确顺序创建对象才能创建好依赖,从叶子结点依赖开始,依次传递到每个父节点依赖,以此类推,直到传递到最高点或是根节点依赖。
事情开始变得复杂了,如果我们还使用工厂方法和生成器来创建类,仅仅是为了传递依赖就要编写相当多复杂的代码,这种代码通常是我们想要避免编写和维护,称为样板文件代码(boilerplate code )。
从这个例子可以看出,我们自己实现的 DI 会导致出现很多样板文件代码。依赖越复杂,你要编写的样板文件代码就越多。DI 很早就流行了,很早就出现了这些问题,因此也就诞生了解决使用 DI 问题的框架。
通过框架可以很轻松地配置依赖,在某些情况下,还能为了创建对象生成工厂和生成器类,从而直接创建易于控制的复杂依赖。
在Android 中应该使用哪个 DI 框架?
由于 DI 很早就流行了,有相当多的 DI 框架可供选择也并不奇怪。在 Java 领域中,我们就有 Spring、Guice 以及最近的 Dagger。所以我们到底应该使用哪种框架?为什么选择它?
Spring 有段时间很流行。它针对解决声明依赖和实例化对象的问题。采用 XML 文件解决这些问题的缺点是,XML 文件几乎像手写代码一样冗长,同时它是在运行时执行验证。
Spring 在解决最初 DI 使用问题的同时也带来了不少问题。
在 Java DI 框架的历史中,Guice 实际上是 Spring 之后的改进版。它舍弃了 XML 配置文件,使用像@Inject 和 @Provides 这样的注解来完成 Java 中的所有配置。
这样看来整体上有了好转,但是依然还存在一些问题。使用 Guice 创建的应用程序在调试和追踪错误时可能有点困难。
另外,它仍然是在运行时验证依赖图并且大量使用反射。对于服务器端应用来说没问题,但是对于大多数启动在低性能设备上的移动应用程序来说代价非常昂贵。
尽管 Guice 前进了一大步,但还是没有解决所有问题,它的设计也并不适合在移动设备上的应用。考虑到这些,Square 公司的一个开发者团队开发了 Dagger 。
Dagger 得名于树状结构的依赖。能更准确地记住这是个依赖图,实际上是有向无环图,或者说是 DAG,因此取名为 DAGger。Dragger 主要是为了解决 Guice 使用中的一些问题,特别是在移动设备上使用 Guice时的问题。
它采取办法将大多数工作量从运行时转移到编译时完成,尽可能移除在过程中的反射,这些措施确实有助于提高移动应用程序的性能。
这些优点都是以牺牲 Guice 中的一些特征为代价,但对于 Android 应用程序来说,Dagger 依然是在正确方向上的进步。
Dagger 基本上算是一个不错的解决方案,是很适合移动设备的 DI 框架。但是 Google 的一个团队还是决定做些改进,所以他们创造了 Dagger 2。
Dagger 2在编译时完成了更多的工作,也很好地移除了反射,最后还能生成比最初版本Dagger 更容易调试的代码。
在我看来,确实没有更好的 Android DI 解决方案了,所以你如果准备使用 DI 框架,我相信 Dagger 2 确实是最容易使用和调试,同时还具有最佳性能的框架了。
在Android 上开始使用DI
所以你打算怎么做呢?幸运的是,Dagger 和 Dagger 2 有强大的拥护者,所以可以找到大量帮助你快速学习的教程和介绍。
在 这里 可以找主要的 Dagger 2 网站,在 这里 可以看到不错的回顾,并且有 Jake Wharton 的精彩介绍。
它涵盖了基本原理,然后深入讨论模块和组件的工作方式,同时也介绍了Dagger2的相关功能。最后,这是一个容易获得的教程清单,可以帮助你坚定地走在使用 Android DI 的道路上:
Dagger 2 精彩概述:
http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android
Dagger 2 的定义和使用方法:
http://konmik.github.io/snorkeling-with-dagger-2.html
Dagger 2 中的作用域:
http://frogermcs.github.io/dependency-injection-with-dagger-2-custom-scopes/
Dagger 2与Espresso 和 Mockito 一起完成测试:
http://blog.sqisland.com/2015/04/dagger-2-espresso-2-mockito.html