DI(依存性注入)とは何か
DI(Dependency Injection)は
「必要な相手(依存先)を、自分で new しないで“外から渡してもらう”考え方」
です。
もっと噛み砕くと、
本来一緒に仕事をする相棒を「自分で勝手に決めて new する」のではなく、
「誰と組むかは外側の人(呼び出し側・設定側)が決めて、コンストラクタなどで渡してもらう」
という設計スタイルです。
こうすることで、
クラス同士の結びつきが弱くなる
差し替えやテストがしやすくなる
設定の変更で挙動を変えられる
といったメリットが生まれます。
まず「悪い例」から:自分で new しまくる世界
典型例:サービスの中で具体クラスを new する
まずは、DI を使っていないコードを見てみます。
final class EmailSender {
void send(String to, String body) {
System.out.println("SEND MAIL to=" + to + " body=" + body);
}
}
final class UserService {
String register(String email) {
// ここで依存先を自分で new している
EmailSender sender = new EmailSender();
sender.send(email, "Welcome!");
return "OK";
}
}
Java一見、何も問題なさそうに見えますが、よく見るとUserService が EmailSender の「具体クラス名」を知っていて、
さらに自分の中で new してしまっています。
この状態だと、いくつか困ることが出てきます。
テストでメール送信だけをフェイクにしたいのに、差し替えられない
本番では外部サービスを使い、テストではコンソール出力にしたい、が簡単にできない
「メール送信方法」を変えたいだけなのに、UserService のコードを書き換えないといけない
つまり、UserService が「依存先の選択」まで抱え込んでしまっていて、
変更にもテストにも弱い構造になっています。
DI を使った「良い例」:誰と組むかは外から決める(重要)
役割をインターフェースにし、依存を外から注入する
同じ機能を DI で書き直してみます。
interface Mailer {
void send(String to, String body);
}
Javaまず「メールを送る」という役割だけをインターフェースにします。UserService は、この Mailer にだけ依存するように書きます。
final class UserService {
private final Mailer mailer; // 役割に依存する
UserService(Mailer mailer) { // コンストラクタで「注入」される
this.mailer = mailer;
}
String register(String email) {
mailer.send(email, "Welcome!"); // 具体クラスは知らない
return "OK";
}
}
Javaここが DI の核心です。
UserService は
「メールを送ることができる相手(Mailer)」
が必要だということだけを宣言し、
「それが具体的に誰なのか(どのクラスか)」は一切知りません。
誰と組ませるかは、外側で決めます。
final class ConsoleMailer implements Mailer {
@Override
public void send(String to, String body) {
System.out.println("SEND MAIL to=" + to + " body=" + body);
}
}
Mailer mailer = new ConsoleMailer();
UserService service = new UserService(mailer);
service.register("test@example.com");
Java本番では別の実装にすることもできます。
final class SmtpMailer implements Mailer {
@Override
public void send(String to, String body) {
// 実際の SMTP 送信処理
}
}
Mailer mailer = new SmtpMailer();
UserService service = new UserService(mailer);
JavaUserService 自体は、一切変更していません。
注入するインスタンスを変えただけで、挙動が変わっています。
これが「依存を外から注入する」ということです。
DI の3つのパターン:どこからどう注入するか
コンストラクタインジェクション(基本形・最重要)
さっき使ったのがコンストラクタインジェクションです。
コンストラクタの引数で、必要な依存オブジェクトを受け取ります。
final class OrderService {
private final Payment payment;
OrderService(Payment payment) { // ここで注入
this.payment = payment;
}
}
Java利点は、
必須の依存をコンストラクタで強制できる(null で作れない)
テストでも同じコンストラクタを使って差し替えできる
フィールドを final にできるので、途中で差し替わらない
という点です。
DI の王道は、基本的にコンストラクタインジェクションだと思ってください。
セッタインジェクション
後から差し込むパターンもあります。
final class ReportService {
private Formatter formatter;
void setFormatter(Formatter formatter) { // セッターで注入
this.formatter = formatter;
}
}
Javaコンストラクタでは必須にせず、「あとから設定する」スタイルです。
オプション依存や、初期化順が難しいときに使うことがありますが、
「設定されていない状態」が存在し得るので、初学者には少し危険です。
基本はコンストラクタ、特別な事情があればセッター、と覚えておくとよいです。
メソッドインジェクション
特定のメソッド呼び出しにだけ、依存を渡すパターンです。
final class ExportService {
void exportAll(UserRepository repo, Output out) { // 引数で依存を注入
// repo からデータを取って out に書く
}
}
Java「この処理のときだけ、この依存が必要」という場合に使います。
設計として依存が局所化されるので、これも有効な手段です。
DI と「テストしやすさ」の関係(重要な深掘り)
DI を使わないと起きること
もう一度、悪い例を見てみます。
final class BadUserService {
String register(String email) {
EmailSender sender = new EmailSender(); // ここで固定
sender.send(email, "Welcome!");
return "OK";
}
}
Javaこのコードをテストするには、本物の EmailSender が実際に動いてしまいます。
場合によっては本当にメールが飛んでしまいます。
テストでは、
メール送信自体は実行したくない
「このメソッドが send を呼んだかどうか」だけ確認したい
というニーズがよくあります。
しかし DI がないと、「中で何を new しているか」が固定されているので、
すり替える余地がありません。
DI を使うとテスト用の偽物を注入できる
先ほどの DI 版の UserService を、テストしたいとします。
final class UserService {
private final Mailer mailer;
UserService(Mailer mailer) {
this.mailer = mailer;
}
String register(String email) {
mailer.send(email, "Welcome!");
return "OK";
}
}
Javaここに、テスト専用の偽物 Mailer を渡します。
final class FakeMailer implements Mailer {
String lastTo;
String lastBody;
@Override
public void send(String to, String body) {
this.lastTo = to;
this.lastBody = body;
}
}
Javaテストコードはこんなイメージになります。
FakeMailer fake = new FakeMailer();
UserService service = new UserService(fake);
service.register("test@example.com");
// ここで fake に記録された内容を検証
assert "test@example.com".equals(fake.lastTo);
assert "Welcome!".equals(fake.lastBody);
Java本物のメール送信は一切行われません。
「ちゃんと Mailer の send が呼ばれたか」「どんな引数で呼ばれたか」だけを検証できます。
DI の大きな価値は、まさにここです。
依存を注入できるようにしておくことで「テスト用の依存」に差し替えることができ、
ユニットテストが現実的なものになります。
DI と DI コンテナ(Spring など)の関係
素の DI とフレームワークの DI
ここまでは、すべて「手動で new して注入」してきました。
Mailer mailer = new SmtpMailer();
UserService service = new UserService(mailer);
Javaこれも立派な DI です。
フレームワークを使っていなくても、思想としての DI は完全に実践できます。
一方、Spring などの DI コンテナは
どのクラスに何を注入するか
どの具体クラスを使うか
といった設定をフレームワーク側が持ち、
アプリ起動時に自動で new +注入までやってくれる仕組みです。
つまり、DI コンテナは「DI の自動配線装置」です。
考え方が分かっていないと「魔法」に見えますが、
やっていることは
インターフェースに対して
コンストラクタ引数に対して
「どのオブジェクトを差し込むか」を決めて、new して注入しているだけです。
まずは手で DI を書けるようになってから、
「それを自動でやってくれるのが DI コンテナなんだな」と理解するのが、いちばん腹落ちします。
設計として DI を考えるときのコツ(まとめ)
DI は「フレームワークのテクニック」ではなく、「設計の態度」です。
自分のクラスの中で、具体クラスを直接 new していないか
インターフェース(役割)に依存できないか
コンストラクタで、必要なものを全部受け取る形にできないか
そのほうがテストで差し替えやすくならないか
と、常に自分に問いかけてみてください。
DI の基本形は、とてもシンプルです。
必要な相手の型をインターフェースで表す
コンストラクタの引数でそれを受け取る
自分の中では new せず、渡されたものに仕事を任せる
この型を守るだけで、クラス同士の結びつきは驚くほど緩くなり、
テストも変更も一気に楽になります。
