依存性注入
いぞんせいちゅうにゅう
依存オブジェクトを外から渡す設計手法。差し替え可能にしてテストと疎結合を実現する。
概要
依存性注入(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 が「それをコードでどう実現するか」という手法、という関係にあります。注入の経路にはコンストラクタのほかに、セッター経由やメソッド引数経由もありますが、依存が揃わないと生成できない(不完全な状態が存在しない)コンストラクタ注入が原則です。
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 の恩恵がもっとも分かりやすく現れるのはテストです。外部のAPI呼び出し、メール送信、時刻取得のような「遅い・不安定・副作用がある」依存をモックに差し替えることで、単体テストは高速で決定的になります。テストしにくいコードは大抵、依存が内部に埋め込まれたコードであり、「テストが書けるか」は設計の結合度を映す鏡になります。
一方で落とし穴もあります。第一に、コンテナの自動解決は魔法になりやすく、配線ミスがコンパイル時ではなく起動時・実行時エラーとして現れ、初学者にはコードの繋がりが追いにくくなります。第二に、何でもかんでも注入する必要はありません。純粋なデータや安定した標準ライブラリまでインターフェースで包むのは、SOLID原則の記事でいう過剰抽象化そのものです。第三に、コンストラクタ引数が7個も8個も並び始めたら、それは DI の問題ではなくクラスの凝集度が低い(責務を抱え込みすぎている)サインです。DI は密結合の痛み止めではなく、依存関係を目に見える形に晒す手法であり、晒された依存の多さは設計を見直す合図として読むべきです。