Hilt は設計を良くもしないし悪くもしない

DI(Dependency Injection)や Hilt は、しばしば「設計を良くする魔法の道具」のように語られます。しかし、本書ではまずこの幻想をはっきり否定しておきます。

Hilt は、設計を良くもしないし、悪くもしない。

Hilt がやってくれるのは、

  • 依存関係の生成を肩代わりすること
  • オブジェクトのライフサイクルを管理すること

だけです。

依存の向きが正しいか、責務が分離されているか、境界が妥当か──それらは Hilt では一切保証されません。むしろ、設計が曖昧なまま Hilt を使うと、その曖昧さがコード全体に固定されてしまいます。


DI 導入の本当の目的

DI を導入する目的は、テスト容易性や疎結合化だと説明されることが多いですが、Android × DDD の文脈では、もう一つ重要な目的があります。

「依存の方向」をコードとして固定すること

  • ViewModel は Domain に依存してよい
  • Domain は Data に依存してはいけない
  • Data は Domain を知らなくてよい

この関係を、new ではなく DI によって表現することで、「間違った依存」を作りにくくなります。Hilt は、この構造を 楽に維持するための道具 です。


モジュール分割による物理的制約

DI 導入するだけでは、間違った依存を制限することはできません。しかし、モジュール分割も合わせて導入することで、制限することができます。

DDD × Android の構成では、多くの場合こうなっています。

domain/
  - usecase
  - repository (interface)

data/
  - repository (implementation)
  - datasource

このとき、

domain モジュールは data モジュールに 依存していない

という Gradle 依存関係を組みます。

その結果:

// domain module
class SomeUseCase {
    private val repo = RealRepository() // 参照できない
}
  • そもそもクラスが見えない
  • import すらできない

という構造になります。


Module 地獄はなぜ起きるのか

Hilt を使い始めると、多くのプロジェクトが Module 地獄に陥ります。

  • Repository ごとに Module
  • UseCase ごとに Module
  • 実装クラス 1 つにつき @Binds 1 行

ファイルは増え、見通しは悪くなり、「DI のためのコード」が本体を侵食していきます。

Module 地獄が起きる原因は、Hilt ではありません。

責務の粒度が揃っていないまま、すべてを DI しようとすること

が原因です。


DI しなくていいものを見極める

すべてを DI する必要はありません。むしろ、DI しない判断も重要な設計です。

DI しなくてよい典型例は次のようなものです。

  • 純粋なデータ変換ロジック
  • stateless な utility
  • 画面専用で再利用されないクラス

これらまで DI すると、依存関係の図は複雑になり、設計の意図が見えなくなります。

DI は 境界を越える依存にだけ使う という意識を持つと、構造は一気にシンプルになります。


Repository と Hilt のちょうどいい距離感

Repository は、Hilt と非常に相性がよい一方で、過剰に DI されがちな存在でもあります。

本書が推奨するのは、次のような形です。

  • Repository は interface として Domain 側に置く
  • 実装は Data 側に置く
  • Hilt Module は Data 側にまとめる

こうすることで、

  • Domain は実装を知らない
  • DI 設定は Data に閉じる

という境界が保たれます。

逆に、Domain 側に Hilt のアノテーションや Module が入り込むと、その時点で境界は崩れ始めます。


BaseActivity / BaseViewModel を作らない理由

DI や共通処理の導入をきっかけに、BaseActivity や BaseViewModel を作りたくなることがあります。

しかし、この設計は次のような問題を引き起こしやすいです。

  • 継承ツリーが責務の混在を招く
  • 「全部入り」ベースクラスが生まれる
  • 差分実装がしづらくなる

本書では、継承ではなく 合成 を強く推奨します。

  • 共通処理はクラスとして切り出す
  • 必要な画面だけがそれを保持する

Hilt は、この合成を支えるための仕組みとして使うのが、最も健全です。


CompositionLocal を使った依存の渡し方

Jetpack Compose では、Hilt 以外にも依存を渡す手段があります。その代表が CompositionLocal です。

CompositionLocal は、

  • UI ツリーに沿った依存の共有
  • 画面スコープでの依存管理

に非常に向いています。

すべてを Hilt で注入しようとするのではなく、

  • アプリ全体の依存 → Hilt
  • UI 階層に閉じた依存 → CompositionLocal

と使い分けることで、依存構造はより自然になります。


DI が設計を壊しているサイン

次のような状態になっていたら、DI が設計を壊し始めているサインです。

  • Module を修正しないと何も追加できない
  • 依存関係を追うだけで疲れる
  • DI 設定が設計意図を説明できていない

その場合は、一度立ち止まって「この依存は本当に境界を越える必要があるのか」を見直すべきです。


この章のまとめ

DI・Hilt は、

  • 設計を表現するための道具であり
  • 設計そのものではない

という位置づけになります。

Android × DDD においては、

  • 依存の方向を固定する
  • 境界を壊さない
  • DI しすぎない

この 3 点を意識するだけで、Hilt は非常に心強い味方になります。

次章では、実際の失敗例を通して、Android × DDD がどこで破綻しやすいのかを見ていきます。