Fragmented Ep. 21: Diving Deep with Dagger

恒例の Fragmented レビュー。今回は第21回の Diving Deep with Dagger について。久しぶりに Donn と Kaushik の2人だけの回だが、カメオ出演(というのか?)で Square の Jesse Wilson が出ている。

Dagger

さて DaggerDagger 2 である。どちらも Android では有名な Dependency Injection (DI) のライブラリだ。

実を言うと、Dagger は自分にとっては「いつか使いたいと思いつつ、使えていない」ライブラリの1つだ。学習コストがそれなりにかかりそうで、ぱっと目につくメリットがそんなにないと二の足を踏んでしまう。良くない癖だ。だから、そんな自分にとっては、このエピソードは Dagger を勉強する良い機会だった。

珍しく最初に雑談があって、Game of Thrones や Halloween の話をしている。Game of Thrones は未見なので意味が分からないが、どうやら Winter Is Coming というのは第1エピソードのタイトルらしい。まぁ、Donn が住んでいるニュージャージーの秋はまさにそんな感じだという話。あと、Halloween で Donn がバナナスーツを着たという話で盛り上がっているのだが、いまいちバナナスーツがどういうものと見做されているか分からない。

…まぁ、どこかで見たことはあるし、非常に馬鹿っぽいというのは分かる。しかし、低い声で Banana と連呼するのは、どういう背景に由来するのだろうか。ちなみに、バナナスーツは海の向こうで人気らしく「なぜ常にバナナスーツを着用しておくべきか」という表があったりする。バナナスーツ、万能だな。

Why Dagger?

バナナスーツではなく Dagger の話だった。雑談の後、DI が必要な背景について、Donn と Kaushik から説明がある。主に2つで、1つは依存関係が深くなってきたときにインスタンスをせっせと生成するのが大変だから、というもので、もう1つはテストのため、とのこと。自分としては面倒というだけの理由で DI を使うことには反対で、どこまで DI でカバーするかは慎重になる必要があると思う。後者については単純に魅力的だ。実際、Mockito でゴリゴリやっている身からすると、DI が欲しくてたまらなくなるときがある。

ところで、この Dagger という名前、由来をご存知だろうか?Jake は Butter Knife とか Kotter Knife とか DI 系のライブラリに剣の名前を付けているが、それもこれも Dagger に倣って付けたものだ。では Dagger は?というと、Kaushik の長々とした説明があって DAG (Directed Acyclic Graph) に由来しているという話が語られる。オブジェクトグラフを解決するアルゴリズムがトポロジカルソートで、解を持つには有向非循環グラフでなければならないから、らしい。なるほど。

探すと、かなり古いが、Jake による Dagger の説明があって、そこでも Dagger の由来について触れられている。それにしても、これ、Jesse が5週間で作ったのか。Google のときの Guice の経験があるとはいえ、すごいな……。

Under The Hood

ここから Dagger のメカニズムの話になるのだが、ここでカメオ出演の Jesse が登場。マップサービスを例に簡単に Dagger がどうやってオブジェクトグラフを構築するのか説明してくれる。

おもしろいのが、Dagger が依存先を解決するときの説明である。たとえば MapServiceGPSServiceNetworkService に依存しているとすると、MapService インスタンスを作るには、依存先のインスタンスを生成しなければならない。さらに NetworkService をインタンス化するときに HttpClient が必要だとすると、それもインスタンス化する必要がある。従って、普通に考えれば、順に依存をたどってはインスタンス化をすれば良いのだが、Dagger では以下のような手順を踏んでいるというのだ。

  1. まずキューを用意して最初に要求された MapService をキューに入れる
  2. キューからクラスを1つ取り出す(最初は MapService になる)
  3. これに依存があって依存先が解決済みでないなら、その未解決の依存先のクラスをキューに入れて、さらにそれ自身も解決できませんでしたというマークを付けてキューに入れる
  4. すでに解決済みなら OK。そのまま解決済みとして別に保存しておく。
    1. に戻る

実際のコードでは、dagger.internal.Linker.java#linkRequested()のあたりがこの箇所に相当する。この133行目あたりにブレークポイントを置いて、dagger.InjectionTest.javaを実行すればキューがどのように変化するか分かって面白い。

あと、コードを読むと分かるが、ここではインスタンス化まではやっていない。Bidning という与えられたクラスをどのようにインスタンス化するか、という処理を確定していくだけなのだ。実際のインスタンス化は dagger.ObjectGraph#inject(T)dagger.ObjectGraph#get(Class<T>) のタイミングで行われる。

Jesse によると、このようなアルゴリズムにしているのは循環依存を避けるためと依存先を取得するプロセスを “tighter” に制御したいから、らしい。実際、たとえば A → B → A という依存があっても、上の linkRequested() は動く。もちろん、何も考えていないとインスタンス化の際に StackOverflowError が起きるが、Provider などをかませていれば大丈夫なのだろう。

話は脱線するが、Dagger のコードを読んでいると、よく出来ていることに感心する。最初 Binding とか key とかよく分からない用語が出てくるのだが、それらの概念が理解できると、途端にすべての見通しが良くなる。最初はちょっと面倒なのだが、デバッグ実行しつつ各クラスやメソッドの JavaDoc を読んでいくとおおまかな挙動が分かるだろう。

Brief Explanation and Some Pitfalls

Jesse が退場した後は、Kaushik と Donn による Dagger と Dagger 2 の概説になる。とはいえ、Dagger 2 についてはそんなに触れられていなくて、主に Dagger の一般的な話と使う際の注意点のような話だ。いくつか抜粋すると、以下のようになる。ざっとしたまとめなので、Dagger を全然知らない人は、Dagger の公式ページを先に読んでおくと良いかもしれない。

ObjectGraph の説明

Dagger では ObjectGraph がすべての元になる。ObjectGraph が依存関係とそれをどう解決するか知っていて、#inject(T) もしくは #get(Class<T>) を呼べば依存を解決してくれる。Daggger 2 には、この ObjectGraph がなくて、代わりに Component がある。

@Module@Provides の説明

@Module は、依存関係の解決方法を提供するクラスを表すアノテーション。その内部の @Provides でアノテートされたメソッドで各クラスのインスタンス化の処理を指示している。これはコードを見た方が早いだろう。

@Module(
  injects = CoffeeApp.class
)
class DripCoffeeModule {
  @Provides Heater provideHeater() {
    return new ElectricHeater();
  }

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

注意点としては、基本的に injects 属性が必須なこと。つまり、エントリーポイントになるクラス(Andorid だったら ActivityView になるだろう)を指定してやる必要がある。これは一見不要に見えるので、FAQ として「なぜ injects は必要なのか?」と聞かれるらしいのだが、回答は「コンパイル時の検証のため」とのこと(参照: java - Use Dagger modules without the “injects” directive - Stack Overflow)。ついでに library 属性についても触れていて、これが true の場合は他の Module に含まれるのが前提になるので injects の指定は不要とのこと。

なお、Dagger 2 では injects は存在しない。代わりに Component があるので、そこがエントリーポイントを提供するようだ。

overrides の説明

これも @Module の属性だが、この属性が true だと、他の Module@Provides を上書きすることができる。基本的に、あるクラスをインスタンス化する方法は一意でなければならないのだが、この属性を指定したときに限り、その Module 内の @Provides メソッドが既存のものを置き換えることができる。ただし、上書き対象は同じ ObjectGraph 内である必要があるし、overrides = true のモジュールが複数あって衝突した場合もダメ。

この属性は、公式ページにもある通り、テストのときに有用で、この機能がない故に Dagger 2 移行を拒んでいる人もいるらしい。Donn は Dagger 2 にない理由として reflection を避けるためじゃないかと推測していたが、overrides は対象のクラスに対するバインディングを上書きするオプションなので、あまり reflection は関係ないように思う。どちらかというと、Component が自動生成されるせいでカスタム Module を差し込むポイントがないのが問題に見える。 ActivityApplicationComponent をセットするメソッドを用意しても良いけれど、テストのためにプロダクションコードに手を入れるのもイマイチで、Dagger 2 でテストやりにくいのはなるほどと思う。

ちなみに、Square では Dagger 2 は使っていないらしい(参照: Dagger 2 support · Issue #158 · JakeWharton/u2020)。まぁ、どこかでコンパイル時のコード生成も利用していないと言っていたから Dagger から移行するメリットはないのだろう。

complete 属性

これも @Module の属性。デフォルトが true で、その場合、この Module 内で必要になるクラスが、この Module 内で解決できる必要がある。これは、静的解析のためで他の Module に依存している場合は false にしろとのこと。Dagger 2 では、これがデフォルトで false になっている。

Assisted Injection

このネタは、Jesse が GoogleGroup で議論している(Assisted Injection for Dagger - Google Groups)についてで、Dagger にも Dagger 2 にもない機能の話。DI は基本的にコンパイル時に依存関係が確定するものだけれど、ランタイム時に決定したい場合もある。たとえば URL を与えて ImageDownloader というインスタンスを作る場合、URL が分かるまではインスタンス化できないので、ImageDownloader を DI で解決はできない。そこで URL を与えたら ImageDownloader が返ってくるような Factory を DI で生成してやって(ああ、ややこしい)、ランタイム時に URL を与えているけれど、ちょっと冗長。こういう用途を Dagger でサポートできないか、という話。スレッドを斜め読みしたが、最終的にauto/factory · google/autoを使おうぜ、という話で落ち着いたようだ。

インスタンス生成時に必要なパラメータには静的に解決できるものと動的に解決できるものの2種類があって両方をサポートしたい、ということなのだろうけれど、やはり DI でやる話ではないと思う。冗長とはいえ Factory は生成できるのだから、そこを簡略化するために、何か複雑な仕組みを作るのはやり過ぎというものだろう。嫌なら Guice もあるしね。

それはそうと、この AutoFactory はなかなか使い勝手が良さそう。役割が明確だし、JSR-330 準拠のアノテーションが付いたコードを生成できるんだから、「こういうので良いんだよ、こういうので。」という気分にさせてくれる。Dagger も JSR-330 準拠にしておいたおかげで報われるし、良い話なんじゃないだろうか。

Espresso

最後にちらりと Kaushik が触れていたのが、このネタ。Espresso で使うときは ActivityTestRule コンストラクタの3つ目の引数を false にしておいて自分で Activity を起動しないとモックを差し込めないという話。Dagger を使ったテストコードは、chiuki/android-test-demoが参考になるだろう。

まとめ

最初にも書いけれど、自分は Dagger を「いつか使おうと思いつつ、使えていない」ライブラリと思っていたので、今回のエピソードは聞けて良かった。だいたいが公式ページに書いてあるような内容なので、すごく得るものがあったかというとそんなことはないけれど、勉強するきっかけになったのだから、十分。そもそも、こういう外的刺激を期待して聴いているのだから。

Dagger についてはおおよそ挙動が把握できたので、いま関わっているプロジェクトで機会があったら導入を提案してみようと思っている。

Dagger 2 はそれほど踏み込めなかったのだけれど、やはりテストで苦しんでいる人がいるのを見ると、躊躇してしまう。スコープがあるのは良いのだけれど、全部コンパイル時のコード生成で行うという方針が吉と出るのか凶と出るのか。しばらく遠巻きに見ることになりそうだ。