依存関係の管理
依存性逆転の原則(DIP)と依存性注入(DI)を理解し、疎結合な設計を実現する
依存関係とは何か
依存の定義
モジュールAがモジュールBに依存しているとは、「AがBを知っている」「AがBを使用している」状態を指します。
// UserServiceがEmailServiceに依存している例
class UserService {
constructor() {
this.emailService = new EmailService(); // 直接的な依存
}
registerUser(userData) {
// ... ユーザー登録処理
this.emailService.sendWelcome(userData.email);
}
}
依存関係の問題
- UserServiceをテストするにはEmailServiceも必要
- EmailServiceの実装を変更するとUserServiceも影響を受ける
- UserServiceを別のプロジェクトで再利用できない
依存性逆転の原則(DIP)
原則の内容
「上位モジュールは下位モジュールに依存してはならない。どちらも抽象に依存すべきである」
❌ 具体的な実装に依存
// 上位モジュール(ビジネスロジック)
class OrderService {
this.mysqlDb = new MySQLDatabase();
}
createOrder(orderData) {
// MySQLの具体的な実装に依存
this.mysqlDb.query(
'INSERT INTO orders...',
orderData
);
}
}
// 下位モジュール(技術的詳細)
class MySQLDatabase {
query(sql, params) {
// MySQL固有の実装
}
}
✅ 抽象に依存
// 抽象(インターフェース)
interface OrderRepository {
save(order: Order): Promise;
findById(id: string): Promise;
}
// 上位モジュール(抽象に依存)
class OrderService {
constructor(
private repository: OrderRepository
) {}
async createOrder(orderData) {
const order = new Order(orderData);
await this.repository.save(order);
}
}
// 下位モジュール(抽象を実装)
class MySQLOrderRepository implements OrderRepository {
async save(order) {
// MySQL固有の実装
}
async findById(id) {
// MySQL固有の実装
}
}
依存性注入(DI)の実践
DIの3つのパターン
1. コンストラクタ注入
class UserService {
constructor(
private emailService: EmailService,
private userRepository: UserRepository
) {}
async registerUser(userData) {
const user = await this.userRepository.save(userData);
await this.emailService.sendWelcome(user.email);
return user;
}
}
// 使用時に依存を注入
const userService = new UserService(
new SendGridEmailService(),
new PostgresUserRepository()
);
2. セッター注入
class NotificationService {
class NotificationService {
private emailProvider: EmailProvider;
setEmailProvider(provider: EmailProvider) {
this.emailProvider = provider;
}
async notify(message: string) {
if (!this.emailProvider) {
throw new Error('Email provider not set');
}
await this.emailProvider.send(message);
}
}
3. メソッド注入
class ReportGenerator {
generateReport(
data: any[],
formatter: ReportFormatter
) {
const processed = this.processData(data);
return formatter.format(processed);
}
}
// 使用時にフォーマッターを指定
const report = generator.generateReport(
salesData,
new PDFFormatter()
);
依存関係の可視化と分析
依存グラフの理解
// 依存関係の可視化例
/*
[Controller]
↓
[Service]
↓ ↓
[Repository] [EmailService]
↓ ↓
[Database] [SMTP Server]
*/
// 良い依存の方向(上から下へ)
Controller → IService
Service → IRepository, IEmailService
MySQLRepository → Database
SMTPEmailService → SMTPServer
// 避けるべき循環依存
UserService ← → OrderService // ❌
依存関係の原則
- 安定したものに依存する(インターフェースは実装より安定)
- 変わりやすいものに依存しない
- フレームワークより、ビジネスロジックを中心に
- 循環依存を避ける
実践的なDIコンテナの活用
手動DI vs DIコンテナ
手動でのDI(小規模プロジェクト向け)
// 依存関係を手動で構築
function createApp() {
// インフラストラクチャ層
const database = new PostgresDatabase(config.db);
const emailClient = new SendGridClient(config.sendgrid);
// リポジトリ層
const userRepo = new UserRepository(database);
const orderRepo = new OrderRepository(database);
// サービス層
const emailService = new EmailService(emailClient);
const userService = new UserService(userRepo, emailService);
const orderService = new OrderService(orderRepo, userService);
// コントローラー層
const userController = new UserController(userService);
const orderController = new OrderController(orderService);
return { userController, orderController };
}
DIコンテナの使用(大規模プロジェクト向け)
// DIコンテナの設定例(TypeScript + InversifyJS風) const container = new Container(); // インターフェースと実装のバインディング container.bind('IDatabase') .to(PostgresDatabase).inSingletonScope(); container.bind ('IEmailService') .to(SendGridEmailService).inSingletonScope(); container.bind ('IUserRepository') .to(UserRepository); container.bind ('IUserService') .to(UserService); // 使用時 const userService = container.get ('IUserService'); // 依存関係は自動的に解決される
テスタビリティの向上
DIによるテストの簡素化
// テスト用のモック実装
class MockEmailService implements IEmailService {
sentEmails: any[] = [];
async sendWelcome(email: string) {
this.sentEmails.push({ type: 'welcome', email });
}
}
class MockUserRepository implements IUserRepository {
users: User[] = [];
async save(user: User) {
this.users.push(user);
return user;
}
}
// テストコード
describe('UserService', () => {
it('should register user and send email', async () => {
// モックを注入
const mockEmail = new MockEmailService();
const mockRepo = new MockUserRepository();
const service = new UserService(mockRepo, mockEmail);
// テスト実行
await service.registerUser({
email: 'test@example.com',
name: 'Test User'
});
// 検証
expect(mockRepo.users).toHaveLength(1);
expect(mockEmail.sentEmails).toHaveLength(1);
expect(mockEmail.sentEmails[0].email)
.toBe('test@example.com');
});
});