ソフトウェア設計

DI・IoC・DIPの違いを徹底整理

DI・IoC・DIPは、それぞれ異なるレベルの概念です。DIPは何に依存するかを決める原則、IoCは誰が制御権を持つかという方向性、DIは依存性を注入する手法です。この記事では、例や面接での回答例まで含めて違いを整理します。

読了 6 分
DI・IoC・DIPの違いを徹底整理のカバー画像

開発を学んでいると、DI、IoC、DIPという3つの用語に一度は頭を悩ませたことがあるのではないでしょうか。

講義や記事で「IoCコンテナがDIで依存性を注入し、DIPを守る」といった説明をされると、それぞれが何を指すのか混乱し始めます。

結論から言うと、3つは異なるレベルの概念です。DIPは原則、つまりなぜそうするのか。IoCはその原則を実現する大きな方向性、つまり誰が制御権を持つのか。DIはその方向性を実装する具体的な手法、つまりどう注入するのかです。

この記事を読めば、3つがどうつながるかを理解し、面接で聞かれても迷わず答えられるようになります。コード例もできるだけ短くしています。

3つの用語を一行で整理

まずは各用語を一行で覚えてから始めると、ずっと理解しやすくなります。

  • DIP(Dependency Inversion Principle、依存性逆転の原則):具体的な実装ではなく、プロトコルなどの抽象に依存する設計原則
  • IoC(Inversion of Control、制御の反転):自分のコードではなく、フレームワークやコンテナがプログラムの制御フローを担うこと
  • DI(Dependency Injection、依存性注入):必要なオブジェクトを内部で生成せず、外部から渡す手法

3つの関係は次のとおりです。

DIPが目標であり原則だとすれば、IoCはその目標を実現する広い概念です。DIはIoCを実装する具体的な方法の1つです。

つまり、抽象化のレベルはDIP > IoC > DIの順に下がると考えればよいでしょう。

DIPからIoCを経てDI・コールバック・テンプレートメソッドへ分岐する階層図
原則から手法まで、3つの用語が位置するレベル

DIPは「何に依存するか」の問題

DIPはSOLID、つまりオブジェクト指向における5つの設計原則のDに当たる依存性逆転の原則です。

要点は2つです。上位モジュールは下位モジュールに依存せず、どちらも抽象に依存することです。

難しそうに聞こえますが、コードで見ると簡単です。

注文サービスが特定の決済サービス、Kakao Payに直接依存している悪い例を見てみましょう。

// 悪い例:具体クラスに直接依存
class OrderService {
    private let pay = KakaoPay() // 決済サービスを変えるなら、ここを大きく修正する必要がある
}

後でNaver Payに変更するには、OrderServiceのコードを直接修正しなければなりません。決済手段が増えるたびに、上位モジュールが影響を受けます。

DIPはこれを逆転させます。決済という抽象、つまりプロトコルを定義し、それだけに依存させるのです。

protocol PayGateway { func pay(amount: Int) }

class OrderService {
    private let pay: PayGateway // 具体クラスではなく抽象に依存
    init(pay: PayGateway) { self.pay = pay }
}

Kakao PayでもNaver Payでも、PayGatewayを実装するだけでよく、OrderServiceを変更する必要はありません。これが「依存の方向が逆転した」という意味です。


IoCは「誰が制御権を持つか」の問題

IoC、制御の反転は、もう少し広い概念です。

通常、私たちが書くコードでは、自分で流れをすべて制御します。オブジェクトを生成し、メソッドを呼び出し、順序を決めます。

IoCはこの制御権を反転させます。自分が呼び出すのではなく、フレームワークが自分を呼び出す構造です。

「ハリウッドの原則」と呼ばれることもあります。「こちらから連絡しないでください。こちらから連絡します(Don’t call us, we’ll call you)。」

SwinjectのようなDIコンテナでは、自分でオブジェクトを生成する代わりに、コンテナがオブジェクトを生成・管理・接続します。制御権が自分からコンテナへ移るのです。

ここで重要なポイントです。IoCはDIだけではありません。

  • フレームワークがコールバックを呼び出すこと
  • テンプレートメソッドパターン
  • UIKitがviewDidLoadなどのライフサイクルメソッドを代わりに呼び出すこと

これらもすべてIoCの例です。DIはその中でも「依存性を注入する」ことに特化した下位概念にすぎません。

「Don't call us, we'll call you」と書かれたカードが置かれた机の上の古い電話機
制御権が移ること、それがIoCのすべてです

DIは「どのように注入するか」の問題

DI、依存性注入は、IoCを実装する代表的な手法です。

先ほどのDIPの例では、OrderServiceがPayGatewayをイニシャライザで受け取りました。これがDIです。必要なオブジェクトを内部で生成せず、外部から渡します。

注入方法は大きく3つあります。

注入方法 説明 推奨度
コンストラクタ注入 initで依存性を受け取る 最も推奨(不変、必須依存性が明確)
メソッド注入 メソッドのパラメータで受け取る オプショナルな依存性の場合
プロパティ注入 生成後にプロパティへ代入する オプショナルかつvarになるため非推奨

Swiftではイニシャライザ注入が推奨されます。依存性をletで固定できて安全ですし、テスト時に偽オブジェクト(Mock)を渡すのも簡単です。

関係をまとめると、次のようになります。

DIPという原則を守るために、IoCという方向で制御権を渡し、具体的な手段としてDIを使う。

この一文が、3つの関係を最も簡潔に表しています。

ホワイトボード上でWHY・WHO・HOWの3つの箱を矢印でつないだ整理図
なぜ・誰が・どのように、3つの問いに分けると混乱しません

よくある質問

Q. DIとIoCは同じ意味ではありませんか?

いいえ。IoCのほうが広い概念で、DIはIoCを実装する複数の方法の1つです。すべてのDIはIoCですが、すべてのIoCがDIとは限りません。

Q. DIPを守れば、自動的にDIを使うことになりますか?

必ずしもそうではありません。DIPは「抽象に依存する」という原則で、その抽象の実装を外部から渡した時点でDIになります。原則と手法は別物です。

Q. SwinjectのようなライブラリなしでもDIは使えますか?

はい。上のSwift例のように、イニシャライザで直接渡すのもDIです。SwinjectやFactoryはこれを自動化するツールにすぎず、DI自体はライブラリなしでも成立します。


まとめます。DIPはなぜ、つまり原則。IoCは誰が、つまり制御権。DIはどのように、つまり手法です。この3つの問いに分けて覚えれば、もう混乱しません。

異なるレベルの概念だと分かれば、DI関連のコードがまったく違って見えるはずです。この記事で、すっきり整理できていればうれしいです。頑張ってください!

あわせて読みたい