- Hilt は設計を良くもしないし悪くもしない
- DI 導入の本当の目的
- モジュール分割による物理的制約
- Module 地獄はなぜ起きるのか
- DI しなくていいものを見極める
- Repository と Hilt のちょうどいい距離感
- BaseActivity / BaseViewModel を作らない理由
- CompositionLocal を使った依存の渡し方
- DI が設計を壊しているサイン
- この章のまとめ
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 がどこで破綻しやすいのかを見ていきます。