開発プロセス●●●○○

依存性注入

Dependency Injection ・ いぞんせいちゅうにゅう

使う部品を自分で new せず外から渡してもらう設計手法。差し替えが効き、テストと疎結合を実現する。

✦ 30秒で試す — 航海偽物に差し替えたいのに、差し込む口がどこにもないBのほうで部品の用意を忘れたとき、失敗に気づくのは?予想してみる

概要

依存性注入(DI: Dependency Injection)は、あるオブジェクトが必要とする別のオブジェクト(依存)を、自分の内部で生成するのではなく、外側から渡してもらう設計手法です。「注文処理クラスがメール送信クラスを内部で new する」のをやめて、「メール送信の実装をコンストラクタ引数で受け取る」ようにする — やること自体はこれだけの、拍子抜けするほど単純なアイデアです。

しかしこの単純な転換が、設計の性質を大きく変えます。依存を外から渡せるということは、差し替えられるということです。本番ではメールを実際に送る実装を、テストでは送ったふりをするだけの偽物(モック)を渡せます。クラス自身は「何を使うか」の決定権を手放し、宣言された依存だけで動く部品になります。Spring、NestJS、ASP.NET Core など主要なフレームワークの多くがこの手法を中核に据えており、現代のサーバサイド開発ではほぼ空気のような前提になっています。

なぜ生まれたか

クラスが依存を内部で new すると、そのクラスは特定の実装に固定的に縛られます。注文処理のテストをしたいだけなのに本物のメールが飛び、DBに接続できない環境ではインスタンス化すらできない — 2000年前後、自動テストやテスト駆動開発が広まるにつれ、この「差し替えられない密結合」が深刻な障害として認識されるようになりました。結合度の問題が、テストという日常の実務で毎日痛みとして現れたのです。

当時の Java エンタープライズ標準だった EJB は、この問題をフレームワークへの全面依存という重い形で扱っており、その反動として2000年代前半に Spring などの軽量コンテナが登場します。2004年、Martin Fowler が論文「Inversion of Control Containers and the Dependency Injection pattern」で、これらのコンテナがやっていることの本質を「依存性注入」と命名・整理し、手法として定着しました。フレームワークがなくても、コンストラクタで依存を受け取るだけで成立する — この見極めが、DI を特定製品の機能から普遍的な設計手法へと格上げしました。

詳細

仕組み — コンストラクタ注入

もっとも基本的で推奨される形がコンストラクタ注入です。依存をインターフェース型の引数として受け取ります。

interface Notifier {
  send(to: string, message: string): Promise<void>;
}

class OrderService {
  constructor(private readonly notifier: Notifier) {}

  async placeOrder(order: Order) {
    // ...注文を確定する処理...
    await this.notifier.send(order.email, "ご注文を承りました");
  }
}

// 本番: 実際にメールを送る実装を注入
const service = new OrderService(new EmailNotifier());

// テスト: 記録するだけの偽物を注入
const fake = { send: async () => { /* 呼び出しを記録 */ } };
const testService = new OrderService(fake);

OrderService は EmailNotifier の存在を知らず、Notifier という抽象だけを知っています。これは SOLID原則の依存性逆転の原則(DIP)そのものの構図で、DIP が「依存の向きはこうあるべき」という原則、DI が「それをコードでどう実現するか」という手法、という関係にあります。注入の経路にはコンストラクタのほかに、セッター経由やメソッド引数経由もありますが、依存が揃わないと生成できない(不完全な状態が存在しない)コンストラクタ注入が原則です。

内部で new する外から注入するOrderServicenew EmailNotifier実装が内部に固定されるテストでも本物のメールが飛ぶ。差し替えの余地がないEmailNotifier本番用FakeNotifierテスト用モック組み立て役(DIコンテナ)どの実装を使うかをここで決める注入OrderServiceconstructor で Notifier を受け取る
内部生成と依存性注入の比較 — 組み立てを外側に出すことで、実装の選択がクラスの外の関心になる

図の右側で起きていることを、手で確かめてください。下の部品の OrderService には、通知の手段を受け取る差し込み口(コンストラクタの引数)があります。本物の EmailNotifier と、送ったふりをして記録するだけの FakeNotifier を差し替えてテストを走らせ、次に「内部で new する」版に切り替えて同じことを試すと、どこで差し替えの自由が失われるのかが見えます。

⚡ 体験: 差し込み口に、本物と偽物を差し替える
OrderService の作り
テストの中(このプロセス)テストの外(ネットワークの先)OrderService注文を確定し、確認メールを頼むnotifier: Notifier差し込み口(空)組み立て役の手元(テストコード)注文が確定する: 未実行注文が確定する確認メールを1通送る: 未実行確認メールを1通送る宛先が注文者になる: 未実行宛先が注文者になるメールサーバ本物の外部サービスお客さんの受信箱EmailNotifier本物: SMTP で送るFakeNotifier偽物: 記録するだけ
差さっている実装—テスト時間—お客さんに届いた試験メール0通
いまのコード
  1. class OrderService {
  2. constructor(private notifier: Notifier) {} // 差し込み口
  3. placeOrder(o: Order) { /* 確定 */ this.notifier.send(o.email, "ご注文を承りました") }
  4. }
  5. const svc = new OrderService(/* 何を渡す? */) // テストコード=組み立て役

OrderService は、通知の手段をコンストラクタの引数(差し込み口)で受け取ります。まず本物の EmailNotifier を差し込み口へドラッグしてください。差し込むとテストが走ります。

IoC — 制御の反転との関係

DI はしばしば IoC(Inversion of Control、制御の反転)と並べて語られます。IoC は「制御の主導権をアプリケーションコードからフレームワーク側へ移す」という広い概念で、「呼び出すな、こちらから呼ぶ(ハリウッドの原則)」と表現されます。GUI のイベントハンドラや Web フレームワークのルーティングも IoC の一種です。DI はその一形態で、反転させる対象が「依存オブジェクトの生成と結び付けの主導権」に特化したものです。Fowler が命名の際に「IoC コンテナ」という当時の呼び名では漠然としすぎるとして、より具体的な「依存性注入」という語を選んだ、という経緯がこの関係をよく表しています。

DIコンテナ — 組み立ての自動化

依存を外から渡すと、その組み立てを誰かがやる必要があります。小さなアプリなら、エントリポイント付近で手作業で new して配線する「手動DI」で十分です。しかし依存のグラフが深くなると配線コードが膨らむため、それを自動化するのが DI コンテナです。「Notifier が要求されたら EmailNotifier を渡す」という対応関係を登録(またはデコレータや型情報から自動解決)しておけば、コンテナが依存グラフを再帰的に解決してオブジェクトを組み立てます。あわせて、インスタンスを1つだけ共有するシングルトンや、リクエストごとに作り直すスコープといったライフサイクル管理も引き受けます。Java の Spring、TypeScript の NestJS、C# の ASP.NET Core 標準DIなどが代表例です。

サービスロケータとの違い — 依存を隠すか、見せるか

DI と似た目的を持つ手法に、サービスロケータ(Service Locator)があります。クラスの中から ServiceLocator.get(Notifier) のようにグローバルな登録簿へ問い合わせて、必要な部品を取り出すやり方です。実装を登録簿で差し替えられるので、一見すると DI と同じ柔軟さが手に入ります。

違いは、依存が外から見えるかどうかです。コンストラクタ注入なら、new OrderService(notifier) の引数を見るだけで「このクラスは通知手段を必要とする」と分かり、渡し忘れれば生成の時点で失敗します。サービスロケータでは依存がメソッドの奥の呼び出しに隠れ、テストで登録を1つ忘れると、そのコードが実行された瞬間にようやく落ちます。しかも登録簿というグローバルな共有状態を経由するため、テスト同士が登録を上書きし合う事故も起きます。Fowler も2004年の論文で両者を並べ、サービスロケータではクラスが何に依存しているかが見えにくくなる点を違いとして挙げました。のちに Mark Seemann(『Dependency Injection in .NET』の著者)は、この「依存が隠れる」性質を理由にサービスロケータをアンチパターンと呼んでいます。DI コンテナをクラスの中から直接呼び出して部品を取り出すと、DI のつもりで実はサービスロケータになってしまうので注意が必要です。

テストでの威力と落とし穴

DI の恩恵がもっとも分かりやすく現れるのはテストです。外部のAPI呼び出し、メール送信、時刻取得のような「遅い・不安定・副作用がある」依存をモックに差し替えることで、単体テストは高速で決定的になります。テストしにくいコードは大抵、依存が内部に埋め込まれたコードであり、「テストが書けるか」は設計の結合度を映す鏡になります。

一方で落とし穴もあります。第一に、コンテナの自動解決は魔法になりやすく、配線ミスがコンパイル時ではなく起動時・実行時エラーとして現れ、初学者にはコードの繋がりが追いにくくなります。第二に、何でもかんでも注入する必要はありません。純粋なデータや安定した標準ライブラリまでインターフェースで包むのは、SOLID原則の記事でいう過剰抽象化そのものです。第三に、コンストラクタ引数が7個も8個も並び始めたら、それは DI の問題ではなくクラスの凝集度が低い(責務を抱え込みすぎている)サインです。DI は密結合の痛み止めではなく、依存関係を目に見える形に晒す手法であり、晒された依存の多さは設計を見直す合図として読むべきです。

試問

注文を確定して確認メールを送る OrderService のテストです。いまの OrderService は中で本物の EmailNotifier を new しているので、テストを走らせるとお客さんに試験メールが届いてしまいます。枠の中の切り替えで OrderService の作りを変えられ、送ったふりをして記録するだけの FakeNotifier のプラグをドラッグで運べます。

✦ 命題挑戦中

中で EmailNotifier を new している OrderService を、お客さんに試験メールを1通も出さずに、テスト3項目すべて緑で通せ

  • OrderService が通知の手段を外から受け取る作りになっている
  • テストの3項目がすべて緑(試験メールを外に出さない)
⚡ 体験: 差し込み口に、本物と偽物を差し替える
OrderService の作り
テストの中(このプロセス)テストの外(ネットワークの先)OrderService注文を確定し、確認メールを頼むnew EmailNotifier()溶接されて外せない組み立て役の手元(テストコード)EmailNotifier本物(中で new)注文が確定する: 未実行注文が確定する確認メールを1通送る: 未実行確認メールを1通送る宛先が注文者になる: 未実行宛先が注文者になるメールサーバ本物の外部サービスお客さんの受信箱FakeNotifier偽物: 記録するだけ
差さっている実装EmailNotifierテスト時間—お客さんに届いた試験メール0通
いまのコード
  1. class OrderService {
  2. private notifier = new EmailNotifier() // 中で new — 溶接
  3. placeOrder(o: Order) { /* 確定 */ this.notifier.send(o.email, "ご注文を承りました") }
  4. }
  5. const svc = new OrderService() // 外からは何も選べない

「内部で new する」版です。OrderService の中で EmailNotifier を new しているので、差し込み口に本物が溶接されています。FakeNotifier を差し込み口へ運んでみてください。