Dagger 官方文档之Dagger1(译文)

译者注

Dagger2是在Dagger1的基础上升级开发的,所以要学习Dagger2,先了解Dagger1。下文是由Dagger1的官方文档翻译而来。
参考:
原文链接
Dagger1项目链接

介绍

在任何应用中最好的类是那些“干活卖力”的:如BarcodeDecoder,KoopaPhysicsEngineAudioStreamer.这些类持有依赖,可能是一个BarcodeCameraFinder,DefaultPhysicEngineHttpStreamer.
相反的,应用中最不好的类是那些“尸位素餐”的家伙:BarcodeDecoderFactory,CameraServiceLoaderMutableContextWrapper.这些类是和好东西缠绕在一块的管道胶带(These classes are the clumsy duct tape that wires the interesting stuff together)。
Dagger是工厂类的替代者(译者注:工厂类指的是负责创建对象的一些类)。它让你将精力花在感兴趣的类上面(译者注:即”关注点分离”)。声明依赖,指定如何去满足他们,然后主导(ship)你的app.
在javax.inject注解标准上构建,每个类都很容易测试。你不会因为一个FakeCreditCardService要用一堆样板文件去替换RpcCreditCardService.
依赖注入不仅仅是为了测试。它也会使得创建可复用、可替换的模块变得容易。你可以在你的app的所有地方共享一个AuthenticationModule。并且你可以在开发期间运行DevLoggingModule,而在产品阶段运行ProdLoggingModule,从而在每个场景下获得正确的行为。
要了解更多信息,请观看QCon公司的Jesse Wilson 2012年的介绍性演讲。

使用Dagger

我们将通过构建一个咖啡机的例子来演示依赖注入和Dagger.完整的可编译可执行代码,请参见Dagger的咖啡案例

声明依赖

Dagger构造你应用类的实例,并满足(satisfy)他们的依赖关系。它使用javax.inject.Inject注解识别它感兴趣的构造方法或者字段(fields)。
使用@Inject注解Dagger要用来创建类实例的构造方法。需要一个新实例时,Dagger会获取需要的参数值并调用构造器。

class Thermosiphon implements Pump {
  private final Heater heater;

  @Inject
  Thermosiphon(Heater heater) {
    this.heater = heater;
  }

  ...
}

Dagger可以直接注入字段。例中,对于heater字段和pump字段它分别获取一个Heater实例和一个Pump实例。

class CoffeeMaker {
  @Inject Heater heater;
  @Inject Pump pump;

  ...
}

如果你的类中有@Inject标注的字段,但没有@Inject标注的构造器,Dagger会试图使用一个无参构造器(如果存在)。缺少@Inject标注的类不能使用Dagger构造。
Dagger不支持方法注入。

满足依赖

默认情况下,如上文描述那样,Dagger通过构造请求类型的实例满足每个依赖。当你请求一个CoffeeMaker时,它会通过调用new CoffeeMaker()获取一个,然后设置它的可注入字段。
但是,@Inject并不是在任何地方都有效:

  • 接口不能构造。
  • 第三方类不能标注。
  • 可配置的对象必须配置!

对于这些@Inject不足或不合适的地方,我们使用一个由@Provides标注的方法去满足依赖。该方法的返回类型定义了它要满足的依赖。
例如,在任何需要Heater的时候调用provideHeater()

@Provides Heater provideHeater() {
  return new ElectricHeater();
}

@Provides方法可能有他们自己的依赖。下面这个方法在任何需要一个Pump的时候返回一个Thermosiphon:

@Provides Pump providePump(Thermosiphon pump) {
  return pump;
}

所有的@Provides方法必须在模块里。这只是一些有@Module标注的类。

@Module
class DripCoffeeModule {
  @Provides Heater provideHeater() {
    return new ElectricHeater();
  }

  @Provides Pump providePump(Thermosiphon pump) {
    return pump;
  }
}

按照惯例,@Provides方法名以provide为前缀,模块类名以module为后缀。

建图(Building the Graph)

@Inject@Provides标注的类形成了一张通过依赖连接的对象图。通过调用ObjectGraph.create()获取这张图,它可以接受一到多个模块(作为参数):

ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());

为了将这张图用起来,我们需要引导注入。这通常需要注入到命令行APP的主类,或者Android App的Activity类中。在我们的咖啡案例中,使用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项允许在编译时验证整张图。提早检测问题提高了开发速度,并在重构中提出了一些危险源。
既然图已经创建好了,根对象已经注入,我们可以运行我们的咖啡机app了

$ java -cp ... coffee.CoffeeApp
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
 [_]P coffee! [_]P

单例(Singletons)

使用@Singleton标注一个@Provides方法或者可注入类(injectable class)。对象图将会为其所有使用者提供该值的一个单例。

@Provides @Singleton Heater provideHeater() {
  return new ElectricHeater();
}

可注入类的@Singleton注解也用作文档。它提示潜在的维护者该类可以在多线程中共享。

@Singleton
class CoffeeMaker {
  ...
}

懒注解(Lazy Injections)

有时候我们需要对象在使用时实例化。对于任何绑定T,你可以创建一个Lazy,它将实例化推迟到Lazyget()方法的第一次调用。如果T是一个单例,Lazy则为ObjectGraph范围内所有注解提供相同实例。否则,每个注入位(injection site)将得到其专属的Lazy实例。无论如何,任何给定的Lazy实例的调用将返回相同的T底层实例。

class GridingCoffeeMaker {
  @Inject Lazy lazyGrinder;

  public void brew() {
    while (needsGrinding()) {
      // Grinder created once on first call to .get() and cached.
      lazyGrinder.get().grind();
    }
  }
}

提供者注解(Providing Injections)

有时候你需要返回多个实例,而不仅仅是注入的那单独的一个值。当你需要多项(Factories,Builders等等)时,有一个办法是注入一个Provider,而不是T.一个Provider每次调用.get()都会创建一个新实例。

class BigCoffeeMaker {
  @Inject Provider 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();
      ...
    }
  }
}

==注意:== 注入Provider创建的代码可能会让人费解,而且这种设计可能让你的对象图中充满了无范围、无组织的对象( 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. 译者注:smell of 应该是一种形容,像空气一样无处不在)。通常你要使用一个Factory或一个Lazy,或者重组你的代码的生命期(lifetimes)和结构,使之能够仅仅注入一个T.但是在某些情况下,注入Provider可以很省事( Injecting Provider can, however, be a life saver in some cases )。常见用法是当你必须使用一个遗留架构(legacy architecture),它与你的对象的自然生命期不同步(例如servlets 设计成单例,但只在请求指定数据的环境中有效)。

限定符(Qualifiers)

有时候单纯的类型不能确定一个依赖。比如一个复杂的咖啡机应用可能需要为水和加热板分别提供加热器。

在这种情况下,我们添加一个qualifier annotation。任何使用@Qualifier(译者注:元注解)注解的注解都属于这类。这里有一个@Named的声明,就是javax.inject内置的限定符注解。

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
  String value() default "";
}

你可以创建你自己的限定符注解,或者直接使用@Named.通过注解关注的字段或参数应用限定符。他们的类型和限定符注解都将用来标识(identify)依赖。

class ExpensiveCoffeeMaker {
  @Inject @Named("water") Heater waterHeater;
  @Inject @Named("hot plate") Heater hotPlateHeater;
  ...
}

通过注解相应的@Provides方法提供合格值(qualified values)。

@Provides @Named("hot plate") Heater provideHotPlateHeater() {
  return new ElectricHeater(70);
}

@Provides @Named("water") Heater provideWaterHeater() {
  return new ElectricHeater(93);
}

依赖不可以同时使用多个限定符注解(Dependencies may not have multiple qualifier annotations)。

静态注解(Static Injection)

==警告:== 此功能应该有限使用,因为静态依赖难以测试和复用。
Dagger 可以注入静态字段。声明了使用@Inject注解的静态字段的类必须列入module注解的staticInjections.

@Module(
    staticInjections = LegacyCoffeeUtils.class
)
class LegacyModule {
}

使用ObjectGraph.injectStatics()给这些静态字段填充值:

ObjectGraph objectGraph = ObjectGraph.create(new LegacyModule());
objectGraph.injectStatics();

==注意:== 静态注入只适用于直接图中的模块。如果从一个plus()调用图中的injectStatics(),模块上的静态注入在扩展图中将不会发生作用(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 inthe extended graph will not be performed)。

编译时验证

Dagger包含一个验证(validates)模块和注入的注解处理器(annotaion processor)。这个处理器。这个处理器非常严格,一旦出现任何无效或不完整的绑定都会引发编译错误。例如下面这个模块缺少对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添加一个@Provide方法或者将该模块标记为未完成(incomplete)可以修复这个问题。我们允许未完成模块缺失依赖项。

@Module(complete = false)
class DripCoffeeModule {
  @Provides Heater provideHeater(Executor executor) {
    return new CpuHeater(executor);
  }
}

模块提供了被注入类没有使用的类型也会引发错误。

@Module(injects = Example.class)
class DripCoffeeModule {
  @Provides Heater provideHeater() {
    return new ElectricHeater();
  }
  @Provides Chiller provideChiller() {
    return new ElectricChiller();
  }
}

因为模块注入的Example只是用了Heater,javac拒绝了没有使用到的绑定:

[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标记为library

@Module(
  injects = Example.class,
  library = true
)
class DripCoffeeModule {
  @Provides Heater provideHeater() {
    return new ElectricHeater();
  }
  @Provides Chiller provideChiller() {
    return new ElectricChiller();
  }
}

为了充分利用编译时验证,创建一个包含应用所有模块的模块。注解处理器会检测并报告各个模块的问题。

@Module(
    includes = {
        DripCoffeeModule.class,
        ExecutorModule.class
    }
)
public class CoffeeAppModule {
}

当你在编译路径中包含了Dagger的jar文件,注解处理器自动生效。

编译时代码生成(Compile-time Code Generation)

Dagger的注解处理器可以生成像CoffeeMaker$InjectAdapter.java或者DripCoffeeModule$ModuleAdapter这样的源文件。这些文件是Dagger的实现细节。你不需要直接使用它们,尽管通过注入进行步骤调试时会接触到它们(though they can be handy when step-debugging through an injection)。

模块重写(Module Overrides)

如果多个有竞争关系的提供相同依赖的@Provides方法,Dagger会因为错误导致失败。但是有时候使用开发或测试代码替代产品代码是必要的。在模块注解中使用override = true让你获得相对于其它模块绑定的优先权。
这里Junit测试使用一个来自Mockito的模拟对象覆盖了DripCoffeeModuleHeater的绑定。这个模拟对象注入到CoffeeMaker,然后进入测试。

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();
  }
}

重写最适合应用中的小变动:

  • 在单元测试中使用一个模拟对象代替真实实现。
  • 在开发中用一个假认证代替LDAP认证。

对于更大的变动,通常使用不同模块的组合更简单。

下载(略)

来自Guice的升级(Upgrading from Guice)

Dagger不支持Guice某些引人注意的特性:

  • 注入final字段和private成员。为了Dagger生成代码的最佳性能。可使用构造函数注入来解决这一问题。
  • 饿汉单例(Eager Singletons)。为每个饿汉单例声明静态字段创建一个EagerSingletons类可解决这一问题。
  • 方法注入。
  • Dagger不能构造缺少@Inject注解的类,即使他们有一个无参构造函数。

参与(Contributing, 略)

证书(License, 略)

你可能感兴趣的:(译文)