依存性の注入(DI)と依存性逆転の原則(DIP)の違い|コード例で使い分け
DI(依存性の注入)とDIP(依存性逆転の原則)は名前も略語も似ているため、同じものだと誤解されがちです。しかし片方は「依存オブジェクトを外から渡す実装テクニック」、もう片方は「依存の向きを抽象へ向ける設計原則」で、指しているものが違います。この記事では両者の定義・コード例・比較表・使い分けに加え、DIコンテナやIoC・AOPとの関係、そしてDIを使わないほうがよい場面まで整理します。
まとめ:DIとDIPの違いと使い分け
- DI(依存性の注入)=オブジェクトが必要とする依存を、内部で
newせず外部から受け取る実装手法。コンストラクタ・セッター・メソッドの3方式がある。 - DIP(依存性逆転の原則)=上位モジュールも下位モジュールも抽象(インターフェース)に依存させるSOLID原則の一つ。依存の「向き」を扱う設計指針。
- 両者の関係:DIはDIPを実現する代表的な手段。ただしDIだけを使ってDIPを満たさない(具象を注入する)ことも、DIコンテナなしで手動DIによりDIPを満たすこともできる。
- 迷ったら:まずコンストラクタ注入で必須依存を渡し、差し替えやテストの必要が出た依存だけをインターフェースに逆転させる。単一実装で安定した依存に無理な抽象化は不要。
依存性の注入(DI)とは|依存を外部から渡す実装手法
依存性の注入(Dependency Injection)は、あるクラスが動作に必要とする別のオブジェクト(依存)を、そのクラス自身が生成するのではなく、外部から渡す手法です。new による生成をクラスの外へ追い出すことで、依存の差し替えが可能になり、テスト時にモックへ置き換えられます。
次のコードは、依存を内部で生成する例(結合が固い)と、コンストラクタで受け取る例(DI)の対比です。
// DIなし:UserService が UserRepository を自分で生成している
public class UserService {
private final UserRepository repo = new UserRepository(); // ここで実装に固定される
public User getUser(int id) {
return repo.findById(id);
}
}
// DIあり:依存を外から受け取る(コンストラクタ注入)
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) { // 呼び出し側が実装を選べる
this.repo = repo;
}
public User getUser(int id) {
return repo.findById(id);
}
}
DIなしの例では UserService が UserRepository の具体的な生成に縛られ、テストで別実装に差し替えられません。DIありの例では、本番実装でもモックでも呼び出し側が渡せます。DIは「単一のデザインパターン」というより、上記のように依存の生成責任を外へ移す設計テクニック(複数のパターン群として語られることもある)として理解すると混乱しません。
3つの注入方式:コンストラクタ・セッター・メソッド注入
DIには依存を渡すタイミングと経路で3つの方式があります。用途で使い分けます。
| 方式 | 渡し方 | 向いている依存 |
|---|---|---|
| コンストラクタ注入 | 生成時に引数で渡す | 必須・不変の依存(既定はこれ) |
| セッター注入 | setterメソッドで後から渡す | 任意・後から差し替える依存 |
| メソッド注入 | 使う処理の引数で都度渡す | その処理だけで使う依存 |
既定はコンストラクタ注入です。必須依存を final で受ければ、生成後に依存が欠けた不完全な状態を作れず、不変性も保てます。セッター注入は任意依存や循環依存の回避に限定して使い、多用するとオブジェクトが「未初期化のまま呼ばれる」バグを招きます。
依存性逆転の原則(DIP)とは|SOLID原則の一つ
依存性逆転の原則(Dependency Inversion Principle)は、オブジェクト指向設計のSOLID原則の頭文字「D」にあたる設計原則です。次の2つのルールで構成されます。
- 上位モジュールは下位モジュールに依存してはならない。両者とも抽象に依存すべき。
- 抽象は詳細に依存してはならない。詳細(具象)が抽象に依存すべき。
通常、上位(業務ロジック)が下位(DBやメール送信などの実装)を直接呼ぶと、依存の矢印は上位→下位に向きます。DIPはこの矢印を「逆転」させ、上位が定義した抽象(インターフェース)に下位実装が従う形にします。
// 上位モジュールが所有する抽象
public interface NotificationSender {
void send(String message);
}
// 下位(詳細)が抽象を実装する=依存の向きが逆転する
public class EmailSender implements NotificationSender {
public void send(String message) { // メール送信の詳細
}
}
// 上位モジュールは具象 EmailSender を知らず、抽象だけに依存する
public class UserRegistration {
private final NotificationSender sender;
public UserRegistration(NotificationSender sender) {
this.sender = sender;
}
public void register(String name) {
sender.send("registered: " + name);
}
}
UserRegistration(上位)は EmailSender という具象を一切参照せず、NotificationSender という抽象にのみ依存します。送信方法をSMSやSlackに変えても上位のコードは無変更で済みます。これがDIPの狙う「変更に強い設計」です。DIPはSOLIDの他の原則、とくに拡張に開き変更に閉じるオープンクローズドの原則(SOLID原則)と密接に関係します。
DIとDIPの違いと関係|なぜ混同されるのか
DIとDIPが混同されるのは、略語が似ているうえに、実務で同時に登場するからです。上のDIP例でも依存の受け取りにコンストラクタ注入(DI)を使っており、両者が並走します。しかし役割は別です。
| 観点 | DI(依存性の注入) | DIP(依存性逆転の原則) |
|---|---|---|
| 正体 | 実装手法・テクニック | 設計原則(SOLIDのD) |
| 扱う問題 | 依存を「どこで作り、どう渡すか」 | 依存の「向き」をどちらへ向けるか |
| 成果物 | コンストラクタ引数などの注入点 | 抽象(インターフェース)と依存の逆転 |
| 単独の可否 | 具象を注入すればDIだけでも成立 | 手動配線でもDIコンテナなしで成立 |
つまりDIはDIPを実現する手段の一つですが、DI=DIPではありません。具象クラスをそのままコンストラクタ注入すれば、DIは使っていてもDIP(抽象への依存)は満たしていません。逆に、抽象に依存させたうえで new を main などの入口に集約すれば、DIコンテナを使わなくてもDIPは満たせます。「DIすればDIPも自動で守られる」という理解が、両者を取り違える最大の原因です。
DIコンテナ・IoC・AOPとの関係
DIの周辺には紛らわしい用語が並びます。関係を整理します。
- IoC(制御の反転):オブジェクトの生成や実行の制御を、自前ではなくフレームワーク側へ委ねる広い概念。DIはIoCを実現する一形態です。
- DIコンテナ:依存の生成・注入・ライフサイクル管理を自動化する仕組み。Java系ならSpring、Google Guice、Android向けのDagger/Hiltなどが代表です。手動DIの配線を肩代わりしますが、必須ではありません。
- AOP(アスペクト指向):ログ・トランザクション・認可のような横断的関心事を本処理から分離する別の技術。SpringではDIと一緒に提供されるため混同されがちですが、DIとは目的が異なります。
// DIコンテナ(Spring)に生成と配線を任せる例
@Configuration
public class AppConfig {
@Bean
public NotificationSender notificationSender() {
return new EmailSender();
}
@Bean
public UserRegistration userRegistration(NotificationSender sender) {
return new UserRegistration(sender); // コンテナが依存を解決して渡す
}
}
コンテナは便利ですが、小規模なアプリでは手動配線のほうが依存の流れを追いやすく、学習コストもかかりません。DIの本質はあくまで「依存を外から渡す」ことであり、コンテナはその自動化にすぎない点を押さえておくと導入判断を誤りません。
DIを避けるべき場面|過剰な抽象化のコスト
DIとDIPは強力ですが、あらゆる依存に適用すべきではありません。次のような場合、抽象化はむしろ設計を読みにくくします。
- 実装が1つしかなく、差し替え予定もない依存:テスト用モックも作らないなら、DIのためだけにインターフェースを切るのは過剰です。具象を直接使ってよい場面です。
- 標準ライブラリや値オブジェクトへの依存:
Stringや日付ユーティリティ、単純なデータ構造まで注入すると、注入点が増えるだけで得るものがありません。 - コンテナの自動配線が過剰な小規模プロジェクト:どの実装が注入されているかがコードから読めなくなり、追跡コストが利益を上回ります。
判断基準はシンプルです。「差し替える可能性」か「テストで置き換えたい理由」があるときだけ抽象と注入を導入する。それ以外は素直に new で生成したほうが読みやすくなります。DIP・DIは目的ではなく、変更容易性とテスト容易性という目的のための手段だからです。より体系的に学ぶなら、書籍「なぜ依存を注入するのか DIの原理・原則とパターン」(Mark Seemann・Steven van Deursen 著)が原理から実装パターンまで扱っており、定番の一次資料になります(サンプルコードはC#ですが、原理はJavaを含むオブジェクト指向全般に通用します)。
よくある質問(FAQ)
Q1. DIとDIPは同じものですか?
いいえ。DIは依存を外部から渡す実装手法、DIPは依存を抽象へ向ける設計原則です。DIはDIPを実現する手段の一つで、DI=DIPではありません。
Q2. DIコンテナは必須ですか?
必須ではありません。コンストラクタ引数で依存を手動配線すればDIコンテナなしでDIもDIPも成立します。コンテナは大規模プロジェクトで配線を自動化したいときの選択肢です。
Q3. コンストラクタ注入とセッター注入はどちらを使うべきですか?
既定はコンストラクタ注入です。必須依存を final で受ければ不完全なオブジェクトを作れません。任意依存や循環依存の回避が必要なときだけセッター注入を使います。
Q4. DIとAOPの違いは何ですか?
DIは依存の受け渡しを扱い、AOPはログやトランザクションなど横断的関心事の分離を扱います。SpringなどDIコンテナがAOPも提供するため同一視されがちですが、目的の異なる別技術です。
Q5. DIPとオープンクローズドの原則の関係は?
DIPで上位モジュールを抽象に依存させると、実装の追加・差し替えが既存コードの変更なしで可能になり、拡張に開き変更に閉じるオープンクローズドの原則の達成を支えます。