Java/Android用のDIライブラリであるDagger2について、公式サンプルであるCoffeeアプリをもとに仕組みを説明してみたいと思います。
ただし、Coffeeアプリは若干理解しづらいところがあったので(@Binds
とかLazy
とか)、以下で説明するコードは少し修正を加えています。
Dagger2ユーザガイドの上から真ん中くらいのところ(Singletons and Scoped Bindings)までの知識で理解できるようになっています。
何をしようとしているか
このサンプルがしようとしているのは、Dagger2によってCoffeeMakerクラスのインスタンスを作成することです。
CoffeeMakerクラスは以下に示すような依存関係を持っています。
https://docs.google.com/presentation/d/1fby5VeGU9CN8zjw4lAb2QPPsKRxx6mSwCe9q7ECNSJQ/pub?start=false&loop=false&delayms=3000&slide=id.p
この依存関係を解決しCoffeeMakerインスタンスを得るには、少し手間がかかります。
Heater heater = new ElectricHeater();
Pump pump = new Thermosiphon(heater);
CoffeeMaker coffeeMaker = new CoffeeMaker(heater, pump).maker();
coffeeMaker.brew();
Dagger2を使うと、上記のコードは以下のようになります。
CoffeeMaker coffeeMaker = DaggerCoffeeShop.create().maker();
coffeeMaker.brew();
このように、CoffeeMakerのクライアントコードは依存関係構築の複雑さから開放されます。
以下では、Dagger2がどのようにしてこれらの依存関係を構築しているかについて説明します。
依存の宣言
Daggerは@Inject
アノテーションがついたコンストラクタを使ってインスタンスを生成します。
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
@Override
public void pump() {
if (heater.isHot()) {
System.out.println("=> => pumping => =>");
}
}
}
Thermosiphonインスタンスが要求された場合、上記のコンストラクタを使ってThermosiphonインスタンスが生成されることになります。
ここで、ThermosiphonクラスはHeaterクラスに依存しています。
Daggerはコンストラクタに定義された依存を識別し、これを解決してくれます。
依存関係を満たす
Daggerは要求された型のインスタンスを生成することによって依存関係を満たそうとしますが、@Inject
による依存の宣言がいつも機能するわけではありません。
以下のようなケースでは、@Inject
は機能しません。
- 型がインタフェースである
- 3rdパーティのクラスにはアノテーションがつけられない
- Configureableなオブジェクト(インスタンス化後にsetter等で設定を行うオブジェクトのことだと思われる)は、設定が必要である
Coffeeアプリでは、CoffeeMakerが依存する型(HeaterとPump)はインタフェースとして宣言されており、上記のケースに該当します。
これらのケースでは、@Provides
アノテーションをつけたメソッドによってインスタンスを生成します。
例えばHeaterは以下のように依存を宣言します。
@Provides
アノテーションをつけたメソッドを定義し、戻り値の型にHeaterを指定します。
@Provides
static Heater provideHeater() {
return new ElectricHeater();
}
同様に、Thermosiphonは以下のように宣言します。
@Provides
メソッドは、それ自身も依存を持つことが可能です。
@Provides
static Pump providePump(Thermosiphon pump) {
return pump;
}
@Provides
メソッドは、モジュールに属する必要があります。
モジュールというのは、@Module
アノテーションがつけられたクラスのことです。
@Module
class DripCoffeeModule {
@Provides
static Heater provideHeater() {
return new ElectricHeater();
}
@Provides
static Pump providePump(Thermosiphon pump) {
return pump;
}
}
慣例的に、@Provides
メソッドにはprovideプレフィックスをつけ、モジュールにはModuleサフィックスをつけるようです。
依存関係グラフを構築する
@Inject
や@Provides
によって定義されたクラスやメソッドは、オブジェクトの依存関係グラフを形成します。
このグラフを作成するには、コンポーネントを定義します。
コンポーネントは@Component
アノテーションがつけられたインタフェースで、引数のないメソッドを持っています。
これらのメソッドの戻り値の型が、解決したい型になります。
以下は、CoffeeMakerインスタンスを返すメソッドを持つ、CoffeeShopコンポーネントを定義しています。
配下の依存関係を構築するために、コンポーネントにはモジュールを渡します。
ここではDripCoffeeModuleを渡しています。
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
CoffeeMaker maker();
}
Daggerはこの定義をもとに、CoffeeShopの実装を自動で生成します。
Daggerによって生成された実装クラスは、Daggerというプレフィックスがつけれられます。
ここでは、DaggerCoffeeShopという実装クラスが生成されます。
コンポーネントの実装クラスにはbuilderメソッドが用意されており、このメソッドを使って依存関係を構築していきます。
CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
.dripCoffeeModule(new DripCoffeeModule())
.build();
上記の例ではDripCoffeeModuleインスタンスを手動で生成してビルダーに渡していますが、以下の条件に合致する場合、この処理は不要になります。
- モジュールのデフォルトコンストラクタにアクセスが可能である場合
- モジュールの全ての
@Provides
メソッドがstaticである場合
コンポーネントのは以下のモジュールが全て上記に当てはまる場合、実装クラスはcreateメソッドを持ち、これを使うとビルダーを通すことなく直接インスタンスを生成できます。
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
最終的にCoffeeMakerのクライアントコードは以下のようになります。
CoffeeMaker coffeeMaker = DaggerCoffeeShop.create().maker();
coffeeMaker.brew();
Coffeeアプリのコード(修正版)
公式のCoffeeアプリを少し修正したコードをここに載せます。
上記で説明した範囲の機能のみで実装しています。
※@Singleton
アノテーションのみ後述します。
interface Heater {
void on();
void off();
boolean isHot();
}
interface Pump {
void pump();
}
class ElectricHeater implements Heater {
boolean heating;
@Override
public void on() {
System.out.println("~ ~ ~ heating ~ ~ ~");
this.heating = true;
}
@Override
public void off() {
this.heating = false;
}
@Override
public boolean isHot() {
return heating;
}
}
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
@Override
public void pump() {
if (heater.isHot()) {
System.out.println("=> => pumping => =>");
}
}
}
@Module
class DripCoffeeModule {
@Singleton
@Provides
static Heater provideHeater() {
return new ElectricHeater();
}
@Provides
static Pump providePump(Thermosiphon pump) {
return pump;
}
}
class CoffeeMaker {
private final Heater heater;
private final Pump pump;
@Inject
CoffeeMaker(Heater heater, Pump pump) {
this.heater = heater;
this.pump = pump;
}
public void brew() {
heater.on();
pump.pump();
System.out.println(" [_]P coffee! [_]P ");
heater.off();
}
}
@Singleton
@Component(modules = DripCoffeeModule.class)
public interface CoffeeShop {
CoffeeMaker maker();
}
public class CoffeeApp {
public static void main(String[] args) {
CoffeeMaker coffeeMaker = DaggerCoffeeShop.create().maker();
coffeeMaker.brew();
}
}
@Singleton
アノテーション
Daggerは依存関係解決の際、デフォルトだと型が要求されるたびにそのインスタンスを生成しようとします。
しかし、Coffeeアプリの場合、デフォルトの挙動だと意図したとおりの動きになりません。
CoffeeMakerのbrewメソッドは、ヒーターをオンにし(heater.on())、水をそそぎ(pump.pump())、コーヒーを淹れて(println())、ヒーターをまたオフにするという動きです。
水をそそぐのはヒーターがオンになっているときのみです。
ヒーターがオンになっているかどうかの確認は、Pumpインスタンスが保持しているHeatインスタンスに委譲していますが、この時、CoffeeMakerインスタンスが保持するHeatインスタンスと、Pumpインスタンスが保持しているHeatインスタンスは同じものを参照している必要があります。
先述の通りDaggerは型が要求されるされるごとにインスタンスが生成されてしまうため、このままだと意図した動きになりません。
そこで、@Singleton
アノテーションを使用し、型が要求されたときに常に同じインスタンスを返すように指定することができます。
上記のコードでは、DripCoffeeModuleのprovideHeaterメソッドに@Singleton
アノテーションをつけ、Heaterインスタンスがシングルトンになるように指定しています。
なお、CoffeeShopインタフェースにも@Singleton
アノテーションがついていますが、これはコンポーネントとその配下のインスタンスが同じライフサイクルになるようにするためです。
まとめ
Daggerを理解する最初の一歩となる基礎の部分を説明しました。
ここで説明したのはユーザガイドの最初の部分だけなので、Daggerを実践していくにはまだまだ足りない知識がたくさんあると思いますが、これからDaggerを使っていきたい人、そして数日後の自分のために参考になればと思い書きました。