最終更新:

依存性注入(DI)の仕組み — 注文の「通知係」を外から渡す


注文の通知係を、どこで選ぶ?

中で相手を決める注文処理メールの通知係を作る変えるには内部を修正外から相手を渡す記録係を選ぶ外側のコード注文処理受け取った相手のsendを呼ぶ
外側で通知係を選ぶと、注文処理を変えずに、記録用・表示用などを差し替えられます。依存を消すのではなく、組み立てる場所を分ける設計です。
⏱ 学習時間の目安 読むだけで概要をつかみ、手元で1つの例を確認
📚 前提知識 会話と図だけでも読めます。コードを試す場合は本文の環境を用意してください。
✅ このガイドで学べること
  • 身近な例で仕組みを説明できる
  • 例の入力と結果を比べ、成立条件を確認する
ひよこ ひよこ
依存性注入って、名前が難しくて身構えちゃう。
ペンギン先生 ペンギン先生
通販で「注文を受ける係」と「通知する係」を考えよう。注文の処理に、使ってほしい通知係を外から渡す。それが依存性注入、DIの基本だよ。
ひよこ ひよこ
依存ってどういう意味なの?
ペンギン先生 ペンギン先生
自分の仕事に別の機能が必要ということだよ。注文処理が通知係のsendを呼ぶなら、その通知機能に依存している。DIは依存をなくすのではなく、使う相手を組み立てる場所を分けるんだ。
ひよこ ひよこ
中で通知係を作るのと何が違うの?
ペンギン先生 ペンギン先生
中に特定のメール送信処理を固定すると、変更や試験のたびに注文側のコードへ手を入れやすくなる。外から渡せば、注文の流れを保ったまま、画面に表示する係やテスト用の記録係へ差し替えられるよ。
ひよこ ひよこ
ライブラリーを入れないとできない?
ペンギン先生 ペンギン先生
なくてもできるよ。下の例はJavaScriptのコンストラクターで通知係を受け取るだけ。まずは手動で組み立てると、誰が誰を使うかが見えるんだ。
ひよこ ひよこ
テストでは本当にメールを送らないの?
ペンギン先生 ペンギン先生
記録用の代役を渡し、どんな通知を依頼したか確かめられるよ。ただし、それで本物のメール配送まで確認したことにはならない。実サービスとの接続は別の試験が必要だね。
ひよこ ひよこ
渡す場所はコンストラクターだけ?
ペンギン先生 ペンギン先生
作る時に渡す方法のほか、設定用のメソッドなどで渡す方法もあるよ。必要なものを作成時にそろえたいか、後から変更を許すかを考えて選ぶ。何でも差し替え可能にすれば良いわけではないんだ。
ひよこ ひよこ
DIコンテナーって何をするの?
ペンギン先生 ペンギン先生
部品の作成や受け渡し、寿命の管理などを助ける道具だよ。SpringやDaggerなどでは設定方法や仕組みが違う。部品が増えたら検討できるけど、小さな例に必ず必要というものではないよ。
ひよこ ひよこ
使いすぎると複雑にならない?
ペンギン先生 ペンギン先生
なることもあるよ。受け取る部品が多すぎないか、AとBが互いに必要な循環になっていないかを確認しよう。「注文処理が必要な相手を外から受け取る」と読めることが、最初の目標だね。

まずは「同じ注文処理に、別の通知係」を渡してみる

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!

次は通知係を自作してみてください。設計の言葉を整理するなら依存性注入、クラスを確認するならオブジェクト指向と関数型の比較へ進めます。

参考資料