依存性注入(DI)の仕組み — 注文の「通知係」を外から渡す
注文の通知係を、どこで選ぶ?
- 身近な例で仕組みを説明できる
- 例の入力と結果を比べ、成立条件を確認する
まずは「同じ注文処理に、別の通知係」を渡してみる
DIは「Dependency Injection」の略です。クラスや関数が必要とする別の機能を、外部から受け取る設計を指します。最初はフレームワークの設定より、どこで相手を決めるかに注目しましょう。
次の例は、メールを送らず、通知内容を配列へ記録します。Node.jsが使える環境で、新しい練習用フォルダーにdi-demo.jsとして保存し、node di-demo.jsで実行してください。
class OrderService {
constructor(notifier) {
this.notifier = notifier;
}
finish(orderId) {
const message = `注文 ${orderId} を受け付けました`;
this.notifier.send(message);
}
}
const messages = [];
const recordNotifier = {
send(message) {
messages.push(message);
}
};
const order = new OrderService(
recordNotifier
);
order.finish('order-demo');
console.log(messages);
配列に注文 order-demo を受け付けましたという1件が入ります。注文処理は「sendを持つ通知係」を使い、記録する係を選んで渡すのは外側です。
次に、最後の3行を次へ置き換えてみましょう。注文クラスを直さなくても、通知先がコンソール表示へ変わります。
const consoleNotifier = { send: console.log };
const order = new OrderService(consoleNotifier);
order.finish('order-demo');
この例に実際の注文保存や決済はありません。通知依頼の流れを確かめるための、小さな設計例です。
「使うこと」と「組み立てること」を分ける
| 場所 | この例の役割 |
|---|---|
| OrderService | 渡された通知係へsendを依頼する |
| recordNotifier | 依頼された文を配列に残す |
| 外側のコード | どの通知係を渡すか決める |
特定の通知クラスをOrderServiceの内部で直接作っても、用途が小さく固定されていれば十分な場合があります。DIが役立つのは、通知先を切り替えたい、外部通信をせず注文処理を試験したい、といった境目があるときです。すべてのクラスにインターフェースを作る必要はありません。
もう少し詳しく:コンテナーと依存関係の見通し
コンストラクター注入では、必要な相手が引数から分かります。設定用のメソッドを使う場合は、必要な設定が済む前に使われないよう設計しましょう。依存を受け取る側がコンテナーから自分で検索すると、何が必要かが引数から見えにくくなることもあります。
DIコンテナーを使う場合は、登録だけでなく部品の寿命も確認します。複数の処理で同じ部品を共有してよいのか、終了時に接続を閉じる必要があるのかは、アプリケーションの設計に関わります。
Springではコンストラクター同士の循環依存が解決できずエラーになる場合があります。Daggerは生成したコードで部品を組み立てます。どちらも「外から相手を渡す」考え方を支援しますが、コンテナー全般が同じ方式で動くわけではありません。
覚え方と次の一歩
「DI」って出てきたら「必要な相手を外から渡す」と思えばだいたいOK!
次は通知係を自作してみてください。設計の言葉を整理するなら依存性注入、クラスを確認するならオブジェクト指向と関数型の比較へ進めます。
参考資料
- Martin Fowler:Dependency Injection — 依存先の組み立てを分ける考え方と注入方式
- Spring:Dependency Injection — コンストラクター・setter注入と循環依存
- Dagger:Developer Guide — 生成コードによるDIと差し替え可能な部品