Dagger - 快速的依赖注入器-官方文档翻译
原文 Dagger - 快速依赖注入器(for android and java) 翻译作者将英文分成了两篇文章,本文将它们还原成了一篇。
文章译自dagger的官方说明文档, 不足之处欢迎指正!
引言
在程序中, 最重要的类是那些真正实现功能
的类:比如BarcodeDecoder
(条形码解码器), KoopaPhysicsEngine
(某某引擎), AudioStreamer
(音频流)。这些类往往有些依赖类, 比如BarcodeCameraFinder
(条形码图形识别器), DefaultPhysicsEngine
(默认物理引擎), 和 HttpStreamer
(http输出流).
相反, 程序中最不重要的类是辅助类(take up space without doing much at all): BarcodeDecoderFactory(条形码解析器工厂), CameraServiceLoader(相机服务加载器), 和 MutableContextWrapper(易变环境变量包装器).这些辅助类就像笨拙的胶带一样,将那些重要的功能类连接起来。
Dagger 就是这些工厂类的终结者。 它帮助你专注在那些重要的功能类上。通过声明依赖关系, 指定规则, 构建整个应用程序。
Dagger 构建在标准的javax.inject annotation基础之上, 每一个类都很容易测试。你也不需要为了便于将 RpcCreditCardService 替换为 FakeCreditCardService, 而构建一堆的样板(boilerplate)。
依赖注入
(Dependency injection
)也并不是仅仅为了测试。 它可以使很容易创建 可复用、通用的Module
。在整个应用程序中, 你可以共享同一个AuthenticationModule
。你也可以在开发过程中使用DevLoggingModule
, 而在发布时使用ProdLoggingModule
。
更多信息请移步 watch an introductory talk by Jesse Wilson at QCon 2012.
译者:
上文中有两个概念:
1. 依赖注入, Dependency injection. 也叫控制反转(IoC), 就是通过一定的规则管理对象间的依赖关系。详细概念参考wiki 控制反转
2. 而Dagger组件则通过annotation实现了依赖注入。
Using Dagger
接下来, 我们通过构建一个Coffer Maker 来说明 依赖注入 和 Dagger。你可以下载完整的Coffee Maker示例代码, 编译调试。
声明依赖关系
Dagger 构造应用程序的类对象,并组合其依赖关系。 Dagger使用 javax.inject.Inject
annotation 标记那些构造函数和成员变量需要依赖注入。
Dagger将使用 @Inject 注释的构造函数
创建类对象。 当请求构建新的类对象时, Dagger 将自动获取相应的参数, 并调用构造函数。
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
...
}
Dagger 可以直接注入成员变量。在这个例子中, 它获取Heater对象, 并注入到成员变量heater,同时获取Pump对象并注入到成员变量pump。
class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;
...
}
当类中含有@Inject注释的成员变量, 却没有@Inject注释的构造函数时, Dagger将使用类的默认构造函数。若类中缺少@Inject注释, 该类是不能由Dagger创建的。
Dagger不支持函数注入。
译者:
若是某些类不需要Inject任何对象, 而又希望由Dagger创建该类对象。 这时, 应该至少添加一个@Inject的默认构造函数, 否则会报异常。
实现依赖关系
(Satisfying Dependencies, 实在想不出合适的中文对应)
默认情况下, Dagger 通过构造相应类型的对象来实现依赖关系。当请求一个CoffeMaker对象时, Dagger将调用new CoffeeMaker()构造函数, 并赋值给@Inject标记的成员变量。
但是@Inject并不是在任何情况下都可以:
-
接口类型不能被构造
-
第三方的类不能被注释构造。
-
可配置的对象必须被配置好
译者:
第一条是说, interfaces的注入Dagger是不能直接构造对象的。当然这样了, 给你接口,谁也构造不了对象。
第 二条是说,Third-party classes can't be annotated. Dagger只能构造那些有@Inject注释的类, 即便没有@Inject 成员变量, 也要至少有一个@Inject的构造函数。第三方的类显然不能加入@Inject注释, 因此也不能被Dagger构造.
第三条是 Configurable objects must be configured!, 不明白啥意思, 求大神指教。
对那些使用@Inject效率极低或者awkward的情况, 可以使用@Provides注释函数来实现依赖关系。这些函数的返回类型定义其实现的依赖关系。
例如, 当需要一个Heater时, Dagger将调用provideHeater()函数获取。
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides注释的函数也可以有他们自己的依赖关系。下面这个Provides函数依赖于一个Thermosiphon对象:
@Provides Pump providePump(Thermosiphon pump) {
return pump;
}
所有的@Provides函数必须属于一个Module。这些Module类使用@Module注释。
@Module
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Pump providePump(Thermosiphon pump) {
return pump;
}
}
通常情况下, 约定@Provides函数以provide作为前缀, @Module类以Module作为后缀。
构建ObjectGraph(对象图表)
@Inject 和 @Provides 注释的类构建了一个对象图表。这些对象与对象之间通过依赖关系相互关联。通过函数ObjectGraph.create()
获取这个对象图表, 这个函数可以接受一个或多个Module作为参数:
ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());
我们需要引导注入来使用这个对象图表。 对于命令行程序, 通常需要注入一个主程序类;对于Android程序,通常需要注入activity类。在这个coffer例子中, CoffeeApp被用于引导注入。我们请求这个对象图表构建一个CoffeeApp的对象实例:
class CoffeeApp implements Runnable {
@Inject CoffeeMaker coffeeMaker;
@Override public void run() {
coffeeMaker.brew();
}
public static void main(String\[\] args) {
ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());
CoffeeApp coffeeApp = objectGraph.get(CoffeeApp.class);
...
}
}
这里唯一缺少的是:这个对象图表并不知道CoffeeApp这个可注入类。我们需要在@Module注释中显式的声明。
@Module(
injects = CoffeeApp.class
)
class DripCoffeeModule {
...
}
injects选项使得可以在编译的过程中检查对象图表是有有效, 从而更早的检测问题,以加快开发速度,降低重构时的风险。
现在, 对象图表已经构建好, 根对象也已被注入, 我们就可以运行这个Coffee 程序了。
$ java -cp ... coffee.CoffeeApp
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
\[_\]P coffee! \[_\]P
单例(Singletons)
给@Provides注释的函数或者可注入的类, 添加注释@Singlton, 构建的这个对象图表将使用唯一的对象实例。
@Provides @Singleton Heater provideHeater() {
return new ElectricHeater();
}
可注入类的@Singleton注释也可以作为说明文档, 它提示那些潜在的维护者们:多个线程将共享唯一个对象实例。
@Singleton
class CoffeeMaker {
...
}
译者:
@Singleton 注释对Dagger有效, 也只在一个ObjectGraph中生效。 若是有多个ObjectGraph, 则有多个相应的@Singleton对象。
延迟注入
某些情况下需要延迟初始化一个对象。对任意的对象T来说, 你可以使用Lazy实现延迟初始化。Lazy只有当Lazy's get()函数调用时, 才会初始化T对象。如果T是个单例的对象, Lazy也将使用同一个对象进行注入操作。否则,每次注入都将生成自己的 Lazy对象。 当然, 任何随后调用Lazy.get()函数将返回之前构建好的T对象。
class GridingCoffeeMaker {
@Inject Lazy<Grinder> lazyGrinder;
public void brew() {
while (needsGrinding()) {
// Grinder created once on first call to .get() and cached.
lazyGrinder.get().grind();
}
}
}
提供者注入(PROVIDER INJECTIONS)
有些情况下, 你需要多个对象实例, 而不是仅仅注入一个对象实例。这时你可以利用Provider实现, 每次调用Provider的get()函数将返回新的T的对象实例。
class BigCoffeeMaker {
@Inject Provider<Filter> filterProvider;
public void brew(int numberOfPots) {
...
for (int p = 0; p < numberOfPots; p++) {
maker.addFilter(filterProvider.get()); //new filter every time.
maker.addCoffee(...);
maker.percolate();
...
}
}
}
Dagger作者也提醒:
注入Provider可能使代码易令人混淆, 是代码结构问题的体现, 请参考原文:
Note: Injecting Provider has the possibility of creating confusing code, and may be a design smell of mis-scoped or mis-structured objects in your graph. Often you will want to use a Factory or a Lazy or re-organize the lifetimes and structure of your code to be able to just inject a T. Injecting Provider can, however, be a life saver in some cases. A common use is when you must use a legacy architecture that doesn't line up with your object's natural lifetimes (e.g. servlets are singletons by design, but only are valid in the context of request-specfic data).
限定符(QUALIFIERS)
有些时候,单纯类型是不能够满足指定依赖的需求的。例如,一个复杂的Coffee maker程序可能需要不同的加热器。
在这种情况下,我们可以添加限定符注释. 这种注释本身有一个@Qualifier注释。 下面是javax.inject中@Named的声明代码:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
String value() default "";
}
你也可以创建自己的限定符注释, 也可以直接使用@Named, 给相应的成员变量添加限定符。这些限定符注释将与类型一起标记依赖关系。
class ExpensiveCoffeeMaker {
@Inject @Named("water") Heater waterHeater;
@Inject @Named("hot plate") Heater hotPlateHeater;
...
}
Supply qualified values by annotating the corresponding @Provides method.
@Provides @Named("hot plate") Heater provideHotPlateHeater() {
return new ElectricHeater(70);
}
@Provides @Named("water") Heater provideWaterHeater() {
return new ElectricHeater(93);
}
依赖关系也可以同时有多重限定符注释。
静态注入(STATIC INJECTION)
警告!!!
建议谨慎使用这个特性, 因为静态依赖注入很难测试和复用。
Dagger可以注入静态变量。拥有@Inject静态变量的类必须在@Module的staticInjections中明确说明。
@Module(
staticInjections = LegacyCoffeeUtils.class
)
class LegacyModule {
}
可以使用ObjectGraph.injectStatics()注入静态变量:
ObjectGraph objectGraph = ObjectGraph.create(new LegacyModule());
objectGraph.injectStatics();
Note: Static injection only operates for modules in the immediate graph. If you call injectStatics() on a graph created from a call to plus(), static injections on modules in the extended graph will not be performed.
译者:
建议大家不要使用静态注入, 如Dagger所说, 静态类和静态变量非常难测试和维护。
即便不用Dagger,正常开发时, 也尽量少用静态变量静态函数, 这货看起来都丑!
编译时有效性检查(COMPILE-TIME VALIDATION)
Dagger包含一个annotation 处理器, 这个处理器检查module和注入的有效性。处理器非常严格, 若是有任何绑定是无效或者不完整的, 将引发编译错误。例如, 这个Module缺少Executor的对象绑定:
@Module
class DripCoffeeModule {
@Provides Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}
编译时, javac将提示缺少对象绑定。
\[ERROR\] COMPILATION ERROR :
\[ERROR\] error: No binding for java.util.concurrent.Executor
required by provideHeater(java.util.concurrent.Executor)
为了修正这个错误, 可以添加Executor的@Provides函数, 或者标记该Module为不完整的。不完整的Module允许缺少对象引用。
@Module(complete = false)
class DripCoffeeModule {
@Provides Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}
在Module中若provide的类,没有在injects列表中使用, 将在编译时触发错误。
@Module(injects = Example.class)
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Chiller provideChiller() {
return new ElectricChiller();
}
}
因为Eample.class的注入声明在Module中仅使用了Heater, 而没有使用Chiller, 因此将触发未使用绑定错误:
\[ERROR\] COMPILATION ERROR:
\[ERROR\]: Graph validation failed: You have these unused @Provider methods:
1. coffee.DripCoffeeModule.provideChiller()
Set library=true in your module to disable this check.
若是这个Module提供的对象绑定, 可能被injects列表中以外的类使用, 可以将改Module标记为library, 以避免出错。
@Module(
injects = Example.class,
library = true
)
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Chiller provideChiller() {
return new ElectricChiller();
}
}
为了在编译时检查所有的Module, 可以创建一个Module包含所有的程序Modules, annotation处理器将检测问题, 并报告。
@Module(
includes = {
DripCoffeeModule.class,
ExecutorModule.class
}
)
public class CoffeeAppModule {
}
当你将Dagger's jar文件包含到编译classpath中时, Annotation处理器自动运行。
编译时代码生成(COMPILE-TIME CODE GENERATION)
Dagger的Annotation processor 可以生成源码文件, 例如CoffeeMaker$InjectAdapter.java o, DripCoffeeModule$ModuleAdapter. 这些文件是Dagger的实现细节。你没有必要直接使用这些类文件, 但可以通过这些进行调试。
Module重载(MODULE OVERRIDES)
若对同一个依赖关系有多个@Provides函数, Dagger 将会报错。但在有些情况下,是有必要替换production代码的, 比如测试和开发。 在@Module中可以使用overrides =true , 重载其绑定关系。
下面这个JUnit测试利用Mockito, 重载了DripCoffeeModule的Heater绑定关系。这个Mock对象将inject到CoffeeMake中。
public class CoffeeMakerTest {
@Inject CoffeeMaker coffeeMaker;
@Inject Heater heater;
@Before public void setUp() {
ObjectGraph.create(new TestModule()).inject(this);
}
@Module(
includes = DripCoffeeModule.class,
injects = CoffeeMakerTest.class,
overrides = true
)
static class TestModule {
@Provides @Singleton Heater provideHeater() {
return Mockito.mock(Heater.class);
}
}
@Test public void testHeaterIsTurnedOnAndThenOff() {
Mockito.when(heater.isHot()).thenReturn(true);
coffeeMaker.brew();
Mockito.verify(heater, Mockito.times(1)).on();
Mockito.verify(heater, Mockito.times(1)).off();
}
}
Replacing the real implementation with a mock for unit tests.
Replacing LDAP authentication with fake authentication for development.
For more substantial variations it's often simpler to use a different combination of modules.