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<T>,它將實例化推遲到Lazy<T>get()方法的第一次調用。如果T是一個單例,Lazy<T>則爲ObjectGraph範圍內所有註解提供相同實例。否則,每個注入位(injection site)將得到其專屬的Lazy<T>實例。無論如何,任何給定的Lazy<T>實例的調用將返回相同的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();
    }
  }
}

提供者註解(Providing Injections)

有時候你需要返回多個實例,而不僅僅是注入的那單獨的一個值。當你需要多項(Factories,Builders等等)時,有一個辦法是注入一個Provider<T>,而不是T.一個Provider<T>每次調用.get()都會創建一個新實例。

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

==注意:== 注入Provider<T>創建的代碼可能會讓人費解,而且這種設計可能讓你的對象圖中充滿了無範圍、無組織的對象( Injecting Provider<T> 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<T>或一個Lazy<T>,或者重組你的代碼的生命期(lifetimes)和結構,使之能夠僅僅注入一個T.但是在某些情況下,注入Provider<T>可以很省事( Injecting Provider<T> 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, 略)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章