依存性の注入(DI)とは
依存性の注入(Dependency Injection/DI) とは、クラスが必要とする他のクラス(依存オブジェクト)を、外部から渡す設計パターンのことです。
簡単に言うと、「クラスの中で必要なものを自分で作るのではなく、外から受け取る」ということです。
依存性って何?
まず「依存性」について理解しましょう。あるクラスAが、別のクラスBを使って動作する場合、「クラスAはクラスBに依存している」と言います。
DIを使わない場合の問題点
具体例を見てみましょう。メール送信機能を持つクラスを考えます。
public class UserService {
private EmailService emailService;
public UserService() {
// クラス内部でEmailServiceを直接生成
this.emailService = new EmailService();
}
public void registerUser(String email) {
// ユーザー登録処理
System.out.println("ユーザーを登録しました");
// メール送信
emailService.sendEmail(email, "登録完了");
}
}
public class EmailService {
public void sendEmail(String to, String message) {
System.out.println("メール送信: " + to + " - " + message);
}
}
このコードには以下のような問題があります:
-
テストがしにくい:
UserServiceをテストする際、実際にメールが送信されてしまう -
変更に弱い:
EmailServiceの実装を変えたい場合、UserServiceも修正が必要 -
再利用性が低い: 別のメール送信方法を使いたくても、
UserServiceを書き換える必要がある
DIを使った場合
では、依存性の注入を使った場合はどうなるでしょうか。
// メール送信のインターフェース
public interface IEmailService {
void sendEmail(String to, String message);
}
// 実際のメール送信実装
public class EmailService implements IEmailService {
@Override
public void sendEmail(String to, String message) {
System.out.println("メール送信: " + to + " - " + message);
}
}
// テスト用のダミー実装
public class MockEmailService implements IEmailService {
@Override
public void sendEmail(String to, String message) {
System.out.println("(テスト)メール送信: " + to + " - " + message);
}
}
public class UserService {
private IEmailService emailService;
// コンストラクタで依存オブジェクトを受け取る(これがDI!)
public UserService(IEmailService emailService) {
this.emailService = emailService;
}
public void registerUser(String email) {
System.out.println("ユーザーを登録しました");
emailService.sendEmail(email, "登録完了");
}
}
使用例
public class Main {
public static void main(String[] args) {
// 本番環境
IEmailService emailService = new EmailService();
UserService userService = new UserService(emailService);
userService.registerUser("user@example.com");
// テスト環境
IEmailService mockEmailService = new MockEmailService();
UserService testUserService = new UserService(mockEmailService);
testUserService.registerUser("test@example.com");
}
}
DIの3つの注入方法
依存性を注入する方法は主に3つあります。
1. コンストラクタ注入(推奨)
public class UserService {
private final IEmailService emailService;
// コンストラクタで注入
public UserService(IEmailService emailService) {
this.emailService = emailService;
}
}
メリット
- 必須の依存性を明示できる
- イミュータブル(不変)なオブジェクトを作れる
2. セッター注入
public class UserService {
private IEmailService emailService;
// セッターメソッドで注入
public void setEmailService(IEmailService emailService) {
this.emailService = emailService;
}
}
メリット
- オプショナルな依存性に適している
- 依存性を後から変更できる
3. インターフェース注入
// 注入用のインターフェース
public interface IEmailServiceInjector {
void injectEmailService(IEmailService emailService);
}
public class UserService implements IEmailServiceInjector {
private IEmailService emailService;
@Override
public void injectEmailService(IEmailService emailService) {
this.emailService = emailService;
}
}
メリット
- 注入方法を明示的に定義できる
デメリット
- 実装が複雑になりがち
Spring BootでのDI
実際の開発では、DIコンテナ(DI Container)と呼ばれるフレームワークを使うことが多いです。Spring Bootを例に見てみましょう。
DIコンテナ: 依存性の注入を自動で行ってくれる仕組みのこと。開発者が手動でnewしてオブジェクトを渡さなくても、フレームワークが自動的に必要なオブジェクトを生成し、注入してくれます。
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
@Service
public class EmailService implements IEmailService {
@Override
public void sendEmail(String to, String message) {
System.out.println("メール送信: " + to + " - " + message);
}
}
@Service
public class UserService {
private final IEmailService emailService;
// @Autowiredアノテーションで自動的に注入される
@Autowired
public UserService(IEmailService emailService) {
this.emailService = emailService;
}
public void registerUser(String email) {
System.out.println("ユーザーを登録しました");
emailService.sendEmail(email, "登録完了");
}
}
Spring Bootでは、@Serviceや@Componentなどのアノテーションを付けることで、自動的にDIコンテナがオブジェクトを管理してくれます。
DIのメリット
- テストが簡単: モック(テスト用の偽物)を注入できる
- 疎結合: クラス間の依存関係が緩やかになり、変更の影響が他のクラスに波及しにくくなる
- 再利用性向上: 同じクラスを異なる依存性で使い回せる
- 保守性向上: 変更が局所化され、修正が楽になる
まとめ
- クラス内部で依存オブジェクトを生成せず、外部から受け取る
- テストしやすく、変更に強いコードになる
- Spring BootなどのフレームワークがDIを自動化してくれる