依存関係とは何か

依存の定義

モジュール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');
    });
});