Dagger

Android・Java 対応の高速依存性注入フレームワーク

by Square, Inc.

注意

本日本語訳は、Dagger ユーザガイドの 2013-03-27 時点 からの非公式な翻訳です。最新および正確な情報については、オリジナルのユーザガイドを参照してください。

イントロダクション

アプリケーションで最良のクラスとは、何かを実行するクラスです。 たとえば、バーコード読み取りクラス BarcodeDecoder 、 Koopa 物理エンジンクラス KoopaPhysicsEngine、 音楽ストリーミングクラス AudioStreamerといった類です。こういったクラスは別のクラスへの依存性を持ちます(バーコードカメラファインダクラス BarcodeCameraFinder、 デフォルトの物理エンジンクラス DefaultPhysicsEngine、 HTTPストリーミングクラス HttpStreamer 等々)。

一方で、アプリケーションで最悪のクラスとは、ほとんど何もしないのに、無駄にスペースを取るようなクラスです。たとえば、バーコード読み取りクラスのファクトリ BarcodeDecoderFactory 、カメラサービスのローダ CameraServiceLoader 、可変コンテキストラッパ MutableContextWrapper といったような。これらのクラスは、関心の対象を継ぎ接ぎするための、いわば見栄えの悪いダクトテープみたいなものです。

Dagger は、このような「ファクトリのファクトリ」 FactoryFactory を代替するものです。これを使えば、関心の対象となるクラスに注力することができます。依存性の解決については、依存性の宣言、どのように満たすかの指定、そしてアプリの配信ーこれだけで済みます。

標準仕様である javax.inject アノテーション(JSR-330)を利用することで、クラスのテストの容易性が高まります。もう、製品時の実装クラス RpcCreditCardService を テスト用の偽クラス FakeCreditCardService に取り替えるためだけに、大量のボイラープレートーお決まりのコードーを書く必要はありません。

また、依存性注入( Dependency Injection, DI )は、単にテストに使うためだけのものではありません。 DI によって、再利用可能で、互換性のあるモジュールの作成が容易になります。たとえば、アプリ全体で同じ認証モジュール AuthenticationModule を共有することができます。その上、開発時には開発用ロギングモジュール DevLoggingModule を使い、製品版では製品版ロギングモジュール ProdLoggingModule を使うといったような、それぞれの状況に最適な振る舞いを得ることができます。

詳細については、 Jesse Wilson が QCon 2012 で発表した入門講座をご覧ください(英語)。

Dagger の使い方

本ガイドでは、コーヒーメーカーパッケージの作成を通して、依存性注入と Dagger のデモを行います。コンパイル・実行可能なサンプルコード一式については、 Dagger の coffee パッケージを参照してください。

依存性の宣言

Dagger は、アプリケーション中のクラスのインスタンスを生成し、依存関係を満たします。 javax.inject.Inject アノテーションによって、どのコンストラクタとフィールドが対象となるか判別されます。

あるクラスのインスタンスを生成するのに、特定のコンストラクタを使いたいとしましょう。そのコンストラクタに @Inject とアノテートしてください。新しいインスタンスが要求された場合、 Dagger は必要なパラメータの値を取得してから、このコンストラクタを呼び出します。

class Thermosiphon implements Pump {
  private final Heater heater;

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

  ...
}

フィールドに直接注入することもできます。以下の例では、 heater フィールドに Heater 型のインスタンスを、 pump フィールドに Pump 型のインスタンスを取得します。

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

  ...
}

@Inject アノテーションを持つフィールドがあるのに、 @Inject アノテーションを持つコンストラクタがない場合、引数なしのコンストラクタが(あれば)使用されます。 @Inject アノテーションがないクラスを Dagger で生成することはできません。

Dagger では、メソッド注入(セッター setXXX などのメソッドを通して注入すること)はサポートされません。

依存性の充足

デフォルトでは、前述のように、 要求された型のインスタンスを生成することで、 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 で終わります。

グラフ構築

@Inject@Provides でアノテートされたクラスは、それぞれの依存関係でリンクすると、オブジェクトのグラフを成します。一個以上のモジュールを引数として取る ObjectGraph.create() を使い、このグラフを取得しましょう。

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

グラフを実際に使うには、エントリポイントを作る必要があります。通常は、アプリケーションを開始する main クラスを指定します。ここでの例では、 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(
    entryPoints = CoffeeApp.class
)
class DripCoffeeModule {
  ...
}

さて、グラフの構築とエントリポイントの注入が終わったので、ついにコーヒーメーカーアプリを実行することができます!

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

シングルトン

@Provides メソッドあるいは注入可能なクラスを @Singleton とアノテートしてみましょう。すると、オブジェクトグラフは、すべてのクライアントに対し、その値の単一のインスタンスを用います。

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

注入可能なクラスに @Singleton アノテーションをつけた場合は、ドキュメンテーションとしても役立ちます。将来のメンテナに対して、このクラスは複数のスレッドに共有される可能性があると注記することができるからです。

@Singleton
class CoffeeMaker {
  ...
}

遅延注入

オブジェクトの初期化を遅延する必要に迫られることもあるでしょう。 任意のバインディング T について、 Lazy<T>get() メソッドが最初に呼び出されるまで初期化を遅らせるようなオブジェクト Lazy<T> を生成することができます。 T がシングルトンの場合、 Lazy<T>ObjectGraph 中のすべての注入に対し、同じインスタンスが使われます。それ以外の場合、それぞれの注入箇所について、固有の Lazy<T> インスタンスが取得されます。いずれにせよ、 Lazy<T> 型の任意のインスタンスに対して、続けて呼び出しを行った場合、 T 型の前回までと同一のインスタンスが返されます。

class GridingCoffeeMaker {
  @Inject Lazy<Grinder> lazyGrinder;

  public void brew() {
    while (needsGrinding()) {
      // 豆挽き器 Grinder は get() の最初の呼び出し時に一度だけ生成され、キャッシュされます。
      lazyGrinder.get().grind();
    }
  }
}

Provider 注入

単一の値を注入するだけではなく、複数のインスタンスを返す必要になる場合もあります。このような場合にはいくつか選択肢がありますが( Factory 、 Builder 等々)、一つの選択肢として、 T ではなく Provider<T> を注入する道があります。 Provider<T> は、 .get() が呼び出されるたびに、新しい T のインスタンスを生成します。


class BigCoffeeMaker {
  @Inject Provider<Filter> filterProvider;

  public void brew(int numberOfPots) {
	...
    for (int p = 0; p < numberOfPots; p++) {
      maker.addFilter(filterProvider.get()); // 毎回新しいフィルタを生成します。
      maker.addCoffee(...);
      maker.percolate();
      ...
    }
  }
}

注: Provider<T> の注入によって混沌としたコードを作成してしまう可能性があります。これを使う必要になるということは、グラフ中のオブジェクトのスコープを間違えた・あるいは構造を間違えた徴候を示すデザインの臭いかもしれません。たいていの場合は、代わりに Factory<T> を注入するか、 Lazy<T> を注入するか、あるいは単に T を注入できるようにオブジェクトの生存期間やコードの構造を再構成するほうが良いでしょう。とはいえ、 Provider<T> の注入に命を救われる場合もあります。よくある使用例としては、オブジェクトの自然な生存期間と整合しないような、レガシーなアーキテクチャを使う必要になった場合です(たとえば、サーブレットはデザインとしてはシングルトンですが、リクエスト固有のデータのコンテキストにある間だけ、有効になっています)。

修飾子

型情報だけでは、依存関係を判別するのに十分ではないこともあります。上質なコーヒーメーカーアプリは、水とホットプレートでそれぞれ別の加熱器を使う必要があるかもしれないでしょう。

こういった場合のために、修飾子アノテーションの機能が追加されています。修飾子アノテーションとは、それ自身が @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;
  ...
}

対応する @Provides メソッドをアノテートして、修飾される値を提供します。

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

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

注意点として、一つの対象に複数の修飾子アノテーションをつけることは許可されていません。

静的注入

警告:静的依存性は再利用やテストが難しいので、この機能の使用はなるべく控えるべきです。

Dagger では、 static フィールドに対する注入も可能です。 @Inject アノテーションが付けられた static フィールドを宣言しているクラスを、 モジュールアノテーションの staticInjections にリストしてください。

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

ObjectGraph.injectStatics() を使い、これらの static フィールドに注入値を代入します。

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

コンパイル時検証

Dagger には、モジュールと注入を検証する アノテーションプロセッサ が備わっています。このプロセッサは厳格で、バインディングが一つでも妥当でないか、不完全である場合にコンパイルエラーを生じます。たとえば、下記のモジュールに 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(complete = false)
class DripCoffeeModule {
  @Provides Heater provideHeater(Executor executor) {
    return new CpuHeater(executor);
  }
}

コンパイル時検証を最大限に活用できるよう、アプリケーションのモジュールを全て含むモジュールを作りましょう。アノテーションプロセッサは複数モジュールにわたって問題を検知・報告します。

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

コンパイル時クラスパスに Dagger の jar ファイルを通した場合、アノテーションプロセッサは自動で有効になります。

コンパイル時コード生成

Dagger のアノテーションプロセッサによって、 CoffeeMaker$InjectAdapter.java とか DripCoffeeModule$ModuleAdapter といったような名前のソースファイルが生成されることもあります。これらは Dagger の実装詳細で、直接使う必要はありませんが、注入をステップ実行でデバッグするときには便利でしょう。

モジュールのオーバーライド

同じ依存性に対して、複数の競合する @Provides メソッドがある場合、 Dagger の実行は失敗します。とはいえ、製品コードを開発やテスト用のコードで代替する必要になることもあるでしょう。モジュールアノテーション中で overrides = true と指定すると、他のモジュールによるバインディングに優先させることができます。

以下の JUnit テストでは、 Heater に対する DripCoffeeModule のバインディングが、 Mockito のモックオブジェクトでオーバーライドされています。 このモックは CoffeeMaker に注入され、テストで使用されることになります。

public class CoffeeMakerTest {
  @Inject CoffeeMaker coffeeMaker;
  @Inject Heater heater;

  @Before public void setUp() {
    ObjectGraph.create(new TestModule()).inject(this);
  }

  @Module(
      includes = DripCoffeeModule.class,
      entryPoints = 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 認証を偽の認証で代替する場合。

より本質的な相違がある場合は、モジュールの異なる組み合わせを用いるほうが、シンプルになる場合が多いでしょう。

ビルド時に Dagger を使う

アプリケーションのランタイム環境に dagger-${dagger.version}.jar を含める必要があります。そのほか、コード生成を有効化するために、コンパイル時に dagger-compiler-${dagger.version}.jar を含めてビルドする必要があります。

Maven プロジェクトで利用する場合、 pom.xml の依存関係を記述する節にランタイムを含め、さらに dagger-compiler アーティファクトを "optional" もしくは "provided" の依存性として含めるとよいでしょう。(${dagger.version} を、今ある適当なリリースのバージョン番号に置き換えてください)

<dependencies>
  <dependency>
    <groupId>com.squareup</groupId>
    <artifactId>dagger</artifactId>
    <version>${dagger.version}</version>
  </dependency>
  <dependency>
    <groupId>com.squareup</groupId>
    <artifactId>dagger-compiler</artifactId>
    <version>${dagger.version}</version>
    <optional>true</optional>
  </dependency>
</dependencies>

Guice からアップグレードする際の注意点

Guice の特徴のうち重要なもので、 Dagger がサポートしないものを以下に列挙します。

  • final フィールドや private メンバへの注入。パフォーマンスを最適化するために、 Dagger はコンパイル時にコードを生成します。制限を回避するには、コンストラクタ注入を使ってください。
  • Eager シングルトン。この制限を回避するには、各 eager シングルトンに対して、 static フィールドを宣言する EagerSingletons クラスを作成してください。
  • メソッド注入(セッター setXXX などのメソッドを通して注入すること)。
  • たとえ引数なしのコンストラクタがあったとしても、 @Inject アノテーションがないクラスを Dagger で生成することはできません。

翻訳者一覧

ライセンス

Copyright 2012 Square, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Fork me on GitHub