What Was DI, or What Is It, Still?
仕事のコードベースに Dagger が入っていた。休暇前には入れたい派と入れたくない派が口論していたけれど、入れたい派たる TL が説得しきったぽい。がんばった。
自分は DI に mixed feeling で、ないよりはいいけど今更騒ぐようなものでもないと感じる。たぶんそれは自分が DI 世代の洗礼をうけており, framework があろうがなかろうが DI 的なコードを書くからかもしれない。依存はコンストラクタの引数で渡すし単体テストがいるなら重いクラスはインターフェイスで隠すでしょ? あたりまえじゃん? という。
少し前に The myth of using Scala as a better Java という記事があった。この記事では Scala では Java と違って DI framework もクールなんだよ!みたいな話をしている。Cake pattern で framework を使わないのが Scala の DI に対する態度だと思っていたけれど、今は違うんだな。まあ手書きだと interceptor とかできないよね。たしかに。
などと話題を目にすることが続いたこともあり、少し DI について思いを馳せていた。
DI はもともと testability の文脈から生まれたけれど、DI framework は必ずしも testability のためだけにあるわけでもない。テストを書こうが書くまいが DI framework に有用性はある。とはいえテストのことを忘れて Dagger のような framework をみると中途半端に見えるのも事実。Android 上だと特に煮え切らない。
何が煮え切らないのか。自分は DI framework に何を期待しているのか。少し考えて見る。
DI framwork はオブジェクトグラフ(DAG)構築を支援するツールと言える。更に一歩さがると、Make をはじめとするビルドシステムにならぶ依存関係解決システムであり、つまり Spark や TensorFlow みたいなデータフローの実行系でもある。有向グラフを評価して末端の結果を計算する。その評価プロセスに伴う複雑さを利用者から隠してくれる。
Java の DI framework...というと一般化しすぎで Dagger の話だけれども、その第一のいまいちさは依存関係記述の冗長さだろう。初期 Spring の残念な XML は論外として、Dagger の annotation と Component/Module クラスを使う依存の記述も特段ぱっとしない。Java のクラスや annotation をたよっているせいで冗長。もうちょっとコンパクトにできないのかとおもってしまう。ただこれは DI の限界というよりまともな言語内 DSL を定義する力のない Java の限界かもしれない。Scala の MacWire なんかはちょっとだけマシに見える。
Dagger のいまいちさその 2 は、非同期性をまったく扱ってくれないところ。たとえば Android の Service はインスタンスを非同期にしか bind できない。だから Service を依存関係にもつグラフは Dagger では作れない。一方でこういう非同期性は Android のコードを辛くする原因の大きな部分を占めている。
こういう非同期性の問題を今は Rx が解決してくれているわけだけど、そのせいで依存の解決が Dagger と Rx にまたがってしまう。となると全部 Rx でよくね、という気分になる。でも本来なら Dagger の builder は必要に応じて build() の戻り値を Promise 的なオブジェクトで返してほしいし、値を provide する module 達も同様に依存関係を非同期で解決させてほしかった。あと 3 年くらいしたら開発者もやることなくなってそういう機能をつけはじめそうだけど、今欲しいんだよ!今くれ!せめてどこかのガッツある子が勢いでハックできるように Dagger にはプラグインを機構つけてほしいです。
Dagger のいまいちさその 3 は, 実行時の引数を依存関係に組み込めないこと。たとえばデータベースにクエリを投げるとかってデータベースのクライアントと SQL を引数にとって依存関係を解決するプロセスというか、ある意味で pure function なわけじゃん。それを扱えないとデータフローエンジンとしてはいまいち。
オブジェクトを inject するという DI 本来の原則によるとアプリケーション側からフレームワークに何かを渡せないのは自然だけれど、物事を宣言的にしていきたいデータフロー処理系の立場からみると DI のコンセプトに固執されてもつまらない。もう副作用のないところは全部ひきとってくれよ。要するにモナドだろ(いってみただけです)。
Dagger のいまいちさその 4 は、寿命の始まりの面倒は見てくれるのに寿命の終わりは放置していること。Android では Activity などライフサイクルの終わりに必要な後始末の処理を忘れて error prone にしがち。そこは lifecycle を manage するツールすなわち DI framework が面倒をみるのが筋じゃね?
最近の Dagger には Android 向けの機能が入るという話を聞いたので喜んで見に行くと、メモリプレッシャーが高くなったときに解放する参照を選べるとか書いてあってがっかり。そうじゃなくて! Component を捨てるタイミングで! destroy() を呼んで欲しいんだよ!そういうコードを生成しろ! @Destroy みたいなアノテーションが必要か・・・
Android ライフサイクルの辛さ, マルチコア化に伴う非同期性の蔓延やデータフロー・プログラミングの隆盛など昨今の身辺事情を鑑みつつ改めて DI を見直すと、かつてはすごくキラキラして見えたものが解いていて欲しい問題の 2 割くらいしか片付けてくれない現状にがっかりする。だから盛り上がれないのだろうな。ないよりはあったほうがいいんだけど。DI よりはもっと今時の何かに期待すべきなのかもしれない。
追記
その後 Dagger には Producer というのがあることを学ぶ。これはまさに非同期にオブジェクトを解決する仕組み!だが・・・ ListenenableFuture なんてつかってんじゃえーーー!RxJava に対応しろ!今すぐだ!
いやいいんですよ Guava. でも Guava だけ特別扱いとかおかしくね?今時みんな Andorid で Guava つかってないんでしょ... Netflix の若者とかが PR 送りつけたりして欲しいもんです。はい。