Java
Dagger2

Dagger2の公式サンプルCoffeeを理解する

Java/Android用のDIライブラリであるDagger2について、公式サンプルであるCoffeeアプリをもとに仕組みを説明してみたいと思います。
ただし、Coffeeアプリは若干理解しづらいところがあったので(@BindsとかLazyとか)、以下で説明するコードは少し修正を加えています。
Dagger2ユーザガイドの上から真ん中くらいのところ(Singletons and Scoped Bindings)までの知識で理解できるようになっています。

何をしようとしているか

このサンプルがしようとしているのは、Dagger2によってCoffeeMakerクラスのインスタンスを作成することです。

CoffeeMakerクラスは以下に示すような依存関係を持っています。

image.png
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アノテーションのみ後述します。

Heater.java(公式のまま)
interface Heater {
    void on();
    void off();
    boolean isHot();
}
Pump.java(公式のまま)
interface Pump {
    void pump();
}
ElectricHeater(公式のまま)
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;
    }
}
Thermosiphon(公式のまま)
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 => =>");
        }
    }
}
DripCoffeeModule.java
@Module
class DripCoffeeModule {
    @Singleton
    @Provides
    static Heater provideHeater() {
        return new ElectricHeater();
    }

    @Provides
    static Pump providePump(Thermosiphon pump) {
        return pump;
    }
}
CoffeeMaker(公式のまま)
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();
    }
}
CoffeeShop.java
@Singleton
@Component(modules = DripCoffeeModule.class)
public interface CoffeeShop {
    CoffeeMaker maker();
}
CoffeeApp.java
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を使っていきたい人、そして数日後の自分のために参考になればと思い書きました。