制御の反転 (IoC) とは - DI との関係とフレームワーク設計の原則

制御の反転はフレームワークがアプリケーションコードを呼び出す設計原則。依存性注入 (DI) との関係 / Hollywood 原則 / IoC コンテナの落とし穴と Lambda での実例を解説

設計原則設計パターン

制御の反転とは

制御の反転 (Inversion of Control, IoC) は、プログラムの制御フローをアプリケーションコードではなくフレームワークやコンテナが管理する設計原則である。「ハリウッドの原則 (Don't call us, we'll call you)」とも呼ばれる。

通常のライブラリ利用では、アプリケーションがライブラリを呼び出す。IoC では逆に、フレームワークがアプリケーションのコードを呼び出す。この「制御の方向の反転」が名前の由来だ。

Martin Fowler は 2005 年の解説で、ライブラリとフレームワークの違いをまさにこの点に置いている。ライブラリは呼び出せる関数の集まりで、どれをどの順で呼ぶかはアプリケーション側が決める。フレームワークは設計の骨格をあらかじめ持っていて、利用者はサブクラス化や自作クラスの登録で自分の処理を差し込む。実行時にその差し込み点を呼ぶのはフレームワークの側だ。ライブラリとフレームワークを分ける線は規模ではなく、制御を握っているのがどちらかである。

// 通常: アプリケーション → ライブラリ (アプリが制御)
import { readFileSync } from 'node:fs';

const raw = readFileSync('config.json', 'utf-8');  // 呼ぶ順番とタイミングをアプリが決める
const config = JSON.parse(raw);
applyConfig(config);

// IoC: フレームワーク → アプリケーション (フレームワークが制御)
app.get('/users', (req, res) => {
  // Express がリクエストを受け取り、適切なハンドラを呼び出す
  // アプリは「何をするか」だけを定義し、「いつ呼ばれるか」はフレームワークに委ねる
});

IoC の実現手段

IoC は広い概念であり、複数の手段で実現できる。依存性注入 (DI) はその 1 つにすぎない。

手段仕組み
依存性注入 (DI)依存オブジェクトを外部から注入コンストラクタインジェクション
イベントリスナーイベント発生時にフレームワークがリスナーを呼び出すDOM イベント、EventEmitter
テンプレートメソッド基底クラスがアルゴリズムの骨格を定義し、サブクラスが詳細を実装Android の Activity の onCreate / onResume
コールバック処理完了時にフレームワークがコールバックを呼び出すNode.js の非同期 API
宣言とメタデータフレームワークが宣言を読み取り、適切なコンポーネントを組み立てるSpring のコンポーネント検出と設定クラス

これらの境界は言語やライブラリの流行で動く。React は以前クラスコンポーネントのライフサイクルメソッドでテンプレートメソッド型の差し込み点を提供していたが、公式ドキュメントは 2026 年 8 月時点でクラスコンポーネントを Legacy API に分類し、新しいコードでは関数コンポーネントを勧めている。差し込み方が継承からフック関数の登録へ移っただけで、呼ぶ側がフレームワークであることは変わらない。手段の名前を覚えるより、「自分のコードが呼ばれる側になっているか」で見分けるほうが実務では役に立つ。

Lambda ハンドラーは IoC の典型例

AWS Lambda のハンドラー関数は IoC そのものだ。開発者は「何をするか」(ハンドラーの実装) だけを定義し、「いつ呼ばれるか」(API Gateway のリクエスト、SQS のメッセージ、EventBridge のイベント) は Lambda ランタイムが制御する。

制御が向こう側にあることは、初期化コードの置き場所にも影響する。ハンドラーの外側に書いたコードは最初の呼び出しの前に一度だけ走り、実行環境が生きている間は初期化された状態で残る。AWS のドキュメントは、関数の実行が終わったあとも次の呼び出しを見込んで実行環境をしばらく保持し、再び呼ばれたときはそれを解凍して再利用すると説明している。ハンドラーの外で作ったデータベース接続やクライアントが次の呼び出しでも使える、という定石はここから来ている。

一方で、実行環境がいつ捨てられるかはこちら側では決められない。ランタイムの更新や保守のために数時間おきに終了させられることがあり、連続して呼ばれている関数でも例外ではない。だから外側に置くのは「あれば使い回せる」性質のものに限り、処理の続きを次の呼び出しへ引き渡す前提の設計はしない。制御を渡した相手の都合で環境が消える、というのが IoC を受け入れた代償である。

DI による IoC の実践

依存性注入は IoC の中で最も実務的に重要な手段だ。テスト容易性とモジュールの疎結合を実現する。

ここで本質なのは注入という手続きではなく、依存先を選ぶ権限がどこへ移るかだ。クラスの内部で new を書いた時点で、そのクラスは自分の仕事に加えて具体的な実装を選ぶ責任まで抱えている。コンストラクタ引数で受け取る形にすれば、選ぶのは組み立てを担う側になり、クラス自身は与えられたものを使うだけになる。テストで差し替えられるのはこの権限移動の副産物であって、目的ではない。

組み立てを担う場所は 1 か所へ寄せる。Web アプリなら起動処理、Lambda ならハンドラーモジュールの先頭で依存グラフを組み、以降の層は受け取った依存を使うだけにする。この境界が曖昧なまま各所で new と注入が混ざると、どの実装が実際に動いているかを知るために全ファイルを追う羽目になる。

IoC コンテナ

Java の Spring や .NET の ASP.NET Core には IoC コンテナ (DI コンテナ) が組み込まれている。コンテナが依存関係のグラフを解析し、必要なオブジェクトを自動的に生成・注入する。

TypeScript や Node.js では tsyringe や InversifyJS がコンテナを提供する。どちらも 2026 年 8 月時点で開発が続いており、InversifyJS はリポジトリを monorepo 構成へ移した。ただし Lambda の関数のように 1 つの入口が抱える依存が数個で収まる規模では、手書きのコンストラクタインジェクションで足りることが多い。コンテナを入れるかどうかは、依存グラフの深さと組み立てコードの重複量で判断する。

コンテナには固有の落とし穴が 2 つある。1 つは、登録漏れや循環依存がコンパイル時ではなく解決時に初めて露見することだ。型で守られていた結線が、コンテナを挟んだ途端に実行時エラーへ後退する。起動時に全登録を一度解決してみるチェックを置いておくと、この後退をある程度取り戻せる。

もう 1 つは寿命の混在だ。Microsoft のドキュメントは、寿命の長いサービスが寿命の短いサービスを抱え込んでしまう誤設定を captive dependency と呼んでいる。シングルトンにスコープ付きのサービスを注入すると、シングルトンは一度しか生成されないため、最初に受け取った短命サービスをそのまま持ち続ける。登録が個別には正しく見えるだけに気づきにくく、コンテナも自動では直してくれない。寿命の指定は登録のたびに意識して選ぶ。

よくある誤解

「IoC = DI」

DI は IoC の一手段であり、IoC そのものではない。Express のルーティング、React が副作用フックを呼ぶタイミング、Lambda のイベントハンドラーもすべて IoC だ。コンテナを 1 つも使っていないコードベースでも、フレームワークの上に乗っている限り IoC は既に効いている。

「IoC を使えば自動的にテストしやすくなる」

IoC (特に DI) はテスト容易性の前提条件だが、十分条件ではない。依存を注入可能にしても、インターフェースが巨大すぎたり、副作用が多すぎたりすれば、テストは依然として困難だ。注入口を増やした結果、コンテナを組み立てないと 1 クラスも動かせない状態になり、かえって単体テストが書きにくくなることもある。

さらに掘り下げるなら関連書籍が参考になる。

この記事は役に立ちましたか?

関連用語

関連する記事