DI と DI コンテナは別物
依存性注入はライブラリなしで成立する。IoC との関係、三つの注入方法、Service Locator との違い、Composition Root と DI コンテナの役割を Dart のコードから整理する。
前回まで、Riverpod の provider と状態のライフサイクルを見てきました。けれども、Riverpod を 使うことと依存性注入(Dependency Injection、DI)は同じではありません。特定のライブラリから 話を始めると、そのライブラリが DI という機能を提供しているように見えてしまいます。
そこで今回は Riverpod から一段引きます。DI は何を分ける設計なのか、DI コンテナはその中で何を 手伝う道具なのかを、ライブラリに頼らない Dart のコードから整理します。
§ 01DIライブラリを入れる前に成立している
次のクラスは、設定の保存先をコンストラクタで受け取っています。
class SettingsStore {SettingsStore(this._preferences);final Preferences _preferences;String? loadTheme() {return _preferences.read('theme');}}
これだけで constructor injection、つまりコンストラクタ注入です。SettingsStore は必要な
Preferences を自分で作らず、外から与えられています。DI 用パッケージも、登録ファイルも、
コンテナも登場していませんが、依存を外から注入するという設計はすでに成立しています。
違いは「どのクラスを使うか」を誰が決めるかです。クラスの中で DevicePreferences() を作ると、
設定を使う処理と保存先の選択が結びつきます。コンストラクタで受け取れば、アプリでは端末用、
テストではメモリ用というように、利用側を変えずに実装を選べます。
DI の出発点はコンテナの導入ではありません。依存する側が具体的な実装を作らず、必要なものを 受け取ることです。コンテナを使うかどうかは、そのあとに選ぶ組み立て方の問題です。
§ 02IOC広すぎる言葉から DI を切り出す
DI という名前が整理された経緯は、Martin Fowler の 2004 年の記事にさかのぼります1Martin Fowler, “Inversion of Control Containers and the Dependency Injection pattern”(2004)。IoC という広い語から Dependency Injection を切り出し、三つの注入形態と Service Locator との違いを整理した。。 当時の軽量コンテナは、自分たちの特徴を Inversion of Control(IoC、制御の反転)と呼んでいました。
しかし IoC は、アプリではなくフレームワークがメインループを握る、といった場面にも当てはまる 広い言葉です。Fowler は、コンテナの特別さを IoC だけで説明しても、何の制御を反転したのかが 分からないと指摘しました。そこで、依存する実装を別の組み立て役が与える仕組みに、より具体的な Dependency Injection という名前を採用しました。
したがって DI は IoC と競合する別概念ではなく、依存を取得する制御に焦点を絞った名前です。 「IoC を使っている」で止めず、誰が依存を選び、誰が作り、どちら向きに渡すのかまで見る必要があります。
§ 03FORMS注入には三つの形がある
Fowler は依存を渡す形を三つに分類しました。
| 形 | 依存を渡す場所 | 特徴 |
|---|---|---|
| constructor injection | コンストラクタ | 生成時に必要なものが揃う |
| setter injection | setter | 生成後に依存を設定する |
| interface injection | 注入用メソッド | 利用側が注入用 interface を実装する |
Dart では constructor injection を選ぶと構造が明確です。必須の引数なら、渡し忘れをコンパイル時に
検出でき、受け取った依存を final にすれば、生成後に別の実装へ変わらないことも型に表せます。
setter injection は生成後に差し込める反面、設定されるまで不完全な状態を許します。 interface injection は注入用 interface を利用側へ実装させるため、仕組みへの結合が増えます。 どちらも DI ですが、必須依存を持つ普通の Dart クラスでは constructor injection が素直な既定値です。
§ 04DIRECTION取りに行くか、与えられるか
依存の具体的な実装をクラスから切り離す方法は、DI だけではありません。Service Locator も、実装を
直接 new する代わりに、登録済みのサービスを返す窓口を使います。
class CheckoutService {CheckoutService() : _clock = services.get<Clock>();final Clock _clock;}
この形では CheckoutService が locator に「Clock をくれ」と要求します。依存を探す責任は
クラスの中にあり、実装を読まないと必要なものが見えません。constructor injection なら、Clock を
選ぶ責任はクラスの外にあり、生成時に依存が与えられます。
class CheckoutService {CheckoutService(this._clock);final Clock _clock;}
Fowler が示した本質的な違いは、制御の向きです。Service Locator では利用側が明示的に取りに行き、 DI では要求なしに依存が与えられます。前者は locator 自身にも依存し、必要なサービスを知るには 実装内の呼び出しを探します。後者では、コンストラクタを見れば必要な依存が分かります。
どちらも具体的な Clock 実装との結合は外せます。ただし、別のアプリでも使うクラスが特定の
locator を知っていると、再利用先にも同じ窓口を要求します。DI なら利用側は組み立て方を知らず、
受け取った Clock を使う責任だけを持てます。
§ 05ROOT設定する場所と使う場所を分ける
依存を受け取るクラスが増えると、今度は「誰がそれらを作るのか」という仕事が残ります。その答えを アプリの入口付近へ集めた場所が、Mark Seemann の Composition Root です2Mark Seemann, “Composition Root”(2011)。依存グラフの組み立てをアプリの入口付近に集め、DI コンテナへの参照をその場所だけに閉じ込める方針を定義した。。
void main() {final preferences = DevicePreferences();final settings = SettingsStore(preferences);final clock = SystemClock();final checkout = CheckoutService(clock);runApp(App(settings: settings, checkout: checkout));}
ここでは具体的な実装を選び、生成し、依存グラフを組み立てています。一方、SettingsStore や
CheckoutService はどこで組み立てられたかを知りません。Fowler が重視したサービスの設定と
使用の分離が、コード上の場所として現れています。
通常のコンストラクタ呼び出しだけで組み立てる方法を、Seemann は Pure DI と呼びます。 旧称 “Poor Man's DI” が劣る印象を与えるため、2014 年にこの名前へ改めました3Mark Seemann, “Pure DI”(2014)。DI コンテナを使わない DI を指し、旧称 “Poor Man's DI” に代わる呼び名として提案された。。
DI コンテナは、この Composition Root で登録や生成を手伝う任意のインフラ部品です。依存グラフが 大きいときの組み立てやライフサイクル管理を自動化できますが、コンテナを入れた瞬間に DI が始まる わけではありません。逆に、アプリ中のクラスがコンテナを直接呼べば、利用と設定の分離が崩れます。
Seemann は、コンテナへの参照を Composition Root だけに閉じ込める方針を示しています。 コンテナは組み立てる境界の実装詳細であり、依存を受け取るクラスの API には現れません。
§ 06FLUTTER実行時の型探索に頼れない
Spring や Guice のような環境では、実行時に型情報を調べ、必要な実装を見つけて組み立てる方法が
使われます。Flutter では同じ前提を置けません。Dart に型リフレクションを提供する
dart:mirrors はありますが、Flutter アプリでは利用できません4Flutter 公式 FAQ は production build とバイナリサイズを理由に dart:mirrors を利用不可としている。Dart SDK issue #44489 は、リフレクションが任意のコードを動的に参照できると未使用コードを判定できず、tree shaking が成立しないと説明している。。
Flutter の production build は事前コンパイルされ、使われないコードを tree shaking で除きます。 リフレクションで任意の型を実行時に参照できると、未使用のコードをコンパイラが確定できません。 すべてを使う可能性があるものとして残すことになり、モバイルのバイナリサイズと相性が悪くなります。
そのため Flutter の DI は、通常の Dart コードで依存を組み立てるか、登録を明示するか、コード生成で 組み立て処理を作る形になります。コード生成を使っても、実行時に型を探索する魔法へ変わるわけでは ありません。依存関係をビルド時までにコードとして確定させています。
明示的であることは単なる不便ではありません。コンストラクタに必要な依存が並び、Composition Root に 実装の選択が集まれば、依存グラフを検索と型検査で追えます。Flutter では、この見える形が実行方式の 制約にも、設計の読みやすさにも合っています。
§ 07SUMMARY設計と道具を分ける
- DI は依存を外から受け取る設計。constructor injection だけでも、ライブラリなしで成立する
- IoC は広い概念。DI は依存を取得する制御に焦点を絞った名前
- 注入には constructor / setter / interface の三形態があり、Dart では constructor が基本になる
- Service Locator は利用側が取りに行き、DI は組み立て側から与えられる
- Composition Root は、依存グラフの組み立てをアプリの入口付近へ集める場所
- Pure DI はコンテナを使わない DI。DI コンテナは Composition Root を助ける任意の道具
- Flutter は
dart:mirrorsを使えないため、依存関係は明示的なコードかコード生成で確定させる
今回は状態管理のライブラリから離れ、依存を使う側と組み立てる側を分けました。次回は Riverpod に戻り、どの構造が DI を担い、どの読み方が Service Locator に見えるかを確かめます。
- [1] Martin Fowler, “Inversion of Control Containers and the Dependency Injection pattern”(2004)。IoC という広い語から Dependency Injection を切り出し、三つの注入形態と Service Locator との違いを整理した。 ↩
- [2] Mark Seemann, “Composition Root”(2011)。依存グラフの組み立てをアプリの入口付近に集め、DI コンテナへの参照をその場所だけに閉じ込める方針を定義した。 ↩
- [3] Mark Seemann, “Pure DI”(2014)。DI コンテナを使わない DI を指し、旧称 “Poor Man's DI” に代わる呼び名として提案された。 ↩
- [4] Flutter 公式 FAQ は production build とバイナリサイズを理由に
dart:mirrorsを利用不可としている。Dart SDK issue #44489 は、リフレクションが任意のコードを動的に参照できると未使用コードを判定できず、tree shaking が成立しないと説明している。 ↩