DI(依存性の注入)を丁寧に説明してみた
依存性の注入(Dependency Injection, DI)がわからない人へ
DIは、オブジェクトが必要とする依存関係を「外部から渡す」設計手法のことです。つまり、「どの具体的な実装を使うかを決めるのはオブジェクト自身ではなく、外部のコードが決める」ということです。
とまあこういうことなんですが、そんなこと言われてもよく分からんよ。という人たちのために、頑張って勉強したことをわかりやすくまとめたので、共有してあげようと思う。
今回は、TypeScriptのクラスを使って説明しています。DIそのものがオブジェクト指向をよくするためのSOLID原則の1つということもあるからです。特にDDDではドメイン駆動設計ということでドメインが他の要素に依存しないことを前提としてあげる必要がある。
※SOLID原則知らない人はこれ読んどいてください👇
それでは本題に入りましょう。今回は下記のようなユースケースを想定したオブジェクトを作成していきます。
📱 ユースケース:ショッピングアプリの通知システム
- 目的: 商品を購入したユーザーに通知を送る。
- 課題: 通知方法(メール・SMS・アプリ内通知)を柔軟に変更できるようにしたい。
まずはDIなしバージョンで見てみましょう。ここでは、メール通知クラス(EmailNotifier)とオーダーサービスクラス(OrderService)を作成しています。
DIなし(通知方法を固定)
class EmailNotifier {
sendNotification(message: string): void {
console.log(`📧 Email: ${message}`);
}
}
class OrderService {
private notifier = new EmailNotifier(); // EmailNotifierに直接依存している
placeOrder(user: string, item: string): void {
console.log(`🛒 ${user} has purchased ${item}`);
this.notifier.sendNotification(`Thank you for purchasing ${item}, ${user}!`);
}
}
// 実行
const orderService = new OrderService();
orderService.placeOrder("Alice", "Laptop");
この時、オーダーサービスクラスは、private notifier = new EmailNotifier();
で直接クラスのインスタンスを作成している。
つまり、オーダーサービスクラスはメール通知クラスに依存していると言える。
ではこれがなぜダメなのかみていこう。
❌ 問題点
-
OrderService
はEmailNotifier
に直接依存しているため、通知方法を変更するにはOrderService
のコードを修正しなければならない。 -
あるいは、
SmsNotifier
というクラスを作成して、OrderService
で呼び出さないといけない -
例えば、SMS通知を追加するには、
OrderService
の中身を変更しないといけない。-
private notifier = new EmailNotifier();
をprivate notifier = new SmsNotifier();
にしてあげないといけない。
-
つまり、通知方法の変更をすればいいだけなのに、オーダーサービスにも修正範囲が拡大してしまっている。
DIあり(通知方法を外部から渡す)
typescript
コピーする編集する
// 通知の共通インターフェース
interface Notifier {
sendNotification(message: string): void;
}
// Email通知
class EmailNotifier implements Notifier {
sendNotification(message: string): void {
console.log(`📧 Email: ${message}`);
}
}
// SMS通知
class SMSNotifier implements Notifier {
sendNotification(message: string): void {
console.log(`📲 SMS: ${message}`);
}
}
// アプリ内通知
class AppNotifier implements Notifier {
sendNotification(message: string): void {
console.log(`📱 App Notification: ${message}`);
}
}
// 注文サービス
class OrderService {
private notifier: Notifier; // 依存関係を抽象(Notifier)に依存させる
constructor(notifier: Notifier) {
this.notifier = notifier;
}
placeOrder(user: string, item: string): void {
console.log(`🛒 ${user} has purchased ${item}`);
this.notifier.sendNotification(`Thank you for purchasing ${item}, ${user}!`);
}
}
// 通知方法を変更しながら実行
const emailOrderService = new OrderService(new EmailNotifier());
emailOrderService.placeOrder("Alice", "Laptop");
// 📧 Email: Thank you for purchasing Laptop, Alice!
const smsOrderService = new OrderService(new SMSNotifier());
smsOrderService.placeOrder("Bob", "Smartphone");
// 📲 SMS: Thank you for purchasing Smartphone, Bob!
const appOrderService = new OrderService(new AppNotifier());
appOrderService.placeOrder("Charlie", "Headphones");
// 📱 App Notification: Thank you for purchasing Headphones, Charlie!
✅ DIを使うとどう便利になるのか?
-
通知方法を簡単に切り替えられる
→
OrderService
のコードを一切変更せずに、メール/SMS/アプリ通知を切り替えられる。 -
新しい通知方法を追加しても
OrderService
を変更しなくていい→
Notifier
を実装するPushNotifier
やSlackNotifier
を作るだけで追加できる。 -
テストがしやすくなる
typescript コピーする編集する class MockNotifier implements Notifier { sendNotification(message: string): void { console.log(`✅ [TEST] Notification sent: ${message}`); } } const testOrderService = new OrderService(new MockNotifier()); testOrderService.placeOrder("TestUser", "TestItem");
→ 実際にメールやSMSを送らずに、通知が適切に処理されているかテストできる。
💡 まとめ
DIなし | DIあり | |
---|---|---|
依存の固定 |
OrderService は EmailNotifier に直接依存 |
Notifier に依存し、具体的な通知方法は外部で決める |
変更のしやすさ | 通知方法を変えるたびに OrderService を修正 |
どの通知方法を使うかを簡単に切り替え |
新機能追加 |
OrderService を直接書き換える必要あり |
新しい Notifier を追加するだけでOK |
テストのしやすさ | 実際に console.log が呼ばれるため、テストしづらい |
MockNotifier で通知処理のテストが簡単 |
📌 結論
DIを使うことで、「通知の出し方」を自由に変更できる し、新しい通知方法を追加しても OrderService
を修正する必要がない!
柔軟で変更しやすい設計になるから、アプリが成長しても管理しやすい! 🚀
参考
はじめに|【DDD入門】TypeScript × ドメイン駆動設計ハンズオン
ドメイン駆動設計の定義についてEric Evansはなんと言っているのか[DDD] - little hands' lab
Discussion