Interactive

Dependency Injection

ClaudeCodeはプロンプトに「DI」を含めることで、外部依存をコンストラクタに注入するテスト可能で保守しやすい設計を生成できます。弊社では、モックテストを容易にしたい場合や、外部システムへの依存を抽象化したい場合に依存性注入パターンを使用します。

弊社の推奨ルール

  1. コンストラクタ注入を必須とする - テスト可能性を最優先
  2. インターフェースに依存する - 具象クラスへの直接依存を避ける
  3. 循環依存を避ける - インターフェースで依存を逆転させる
  4. モック注入でテスト - テスト時は実装をモックに差し替える
  5. グローバル変数を排除 - すべての依存を明示的に注入

ClaudeCodeでの利用

DIパターンへの変換

グローバル変数や直接的な依存を抽象化したい場合に使用

「このクラスをDIパターンに書き換えて。依存はコンストラクタで注入するように」

テスト可能な設計への変更

モックを使ったテストができるよう設計を改善する

「テストしやすいようにDIで書き直して」

循環依存の解決

インターフェースを使った依存の逆転で循環参照を解消

「この循環依存をインターフェースで解決して」

推奨するパターン

グローバル変数ではなくDI(依存性注入)を使う

外部依存をテスト可能にするため、コンストラクタで注入するパターンです。グローバル依存はテストが困難で、予期しない副作用を生む原因となります。

// ❌ 推奨しない:グローバル依存
const globalDB = new PostgresDatabase();
const globalLogger = new ConsoleLogger();

class UserService {
  async getUser(id: string) {
    globalLogger.info(`Getting user: ${id}`);
    return globalDB.query(`SELECT * FROM users WHERE id = ?`, [id]);
  }
}

グローバル依存を解決するため、まずインターフェースを定義して依存性注入パターンを実装します。

// ✅ 推奨:依存性注入
interface Database {
  query(sql: string): Promise<any>;
}

interface Logger {
  info(message: string): void;
}

class UserService {
  constructor(
    private readonly db: Database,
    private readonly logger: Logger
  ) {
    Object.freeze(this);
  }
  
  async getUser(id: string) {
    this.logger.info(`Getting user: ${id}`);
    return this.db.query(`SELECT * FROM users WHERE id = ?`, [id]);
  }
}

使用時は実装を注入し、テスト時はモックを注入できます。

// 使用時に依存を注入
const service = new UserService(
  new PostgresDatabase(),
  new ConsoleLogger()
);

// テスト時はモックを注入
const testService = new UserService(
  new MockDatabase(),
  new MockLogger()
);

このパターンにより、実装を変更せずに異なる環境での動作が可能になります。

具象クラスではなくインターフェースに依存する

テストと保守性を向上させるため、抽象インターフェースに依存します。具象クラスへの直接依存は、テストでのモック差し替えを困難にします。

// ❌ 推奨しない:具象クラスに依存
class EmailService {
  constructor(private smtpClient: SMTPClient) {} // 具体実装に依存
  
  async sendEmail(to: string, subject: string) {
    return this.smtpClient.send(to, subject); // 変更が困難
  }
}

インターフェースに依存することで柔軟な実装が可能になります。

// ✅ 推奨:インターフェースに依存
interface EmailProvider {
  send(to: string, subject: string): Promise<void>;
}

class EmailService {
  constructor(private readonly provider: EmailProvider) {
    Object.freeze(this);
  }
  
  async sendEmail(to: string, subject: string) {
    return this.provider.send(to, subject);
  }
}

複数の実装を提供でき、テスト用モックも簡単に作成できます。

// 複数の実装が可能
class SMTPProvider implements EmailProvider {
  async send(to: string, subject: string) {
    // SMTP実装
  }
}

class SendGridProvider implements EmailProvider {
  async send(to: string, subject: string) {
    // SendGrid API実装
  }
}

// テスト用モック
class MockEmailProvider implements EmailProvider {
  async send(to: string, subject: string) {
    console.log(`Mock: ${to} - ${subject}`);
  }
}

インターフェースにより、実装を自由に切り替えることができます。

よくある問題

DIの循環依存

相互に依存するクラスで循環参照が発生する問題です。インターフェースを使用して依存を逆転させることで解決できます。

// ❌ エラー:循環依存が発生
class ServiceA {
  constructor(private serviceB: ServiceB) {}
}

class ServiceB {
  constructor(private serviceA: ServiceA) {} // 循環参照エラー
}

循環依存を解決するため、インターフェースで依存を逆転させます。

// ✅ 解決:インターフェースで依存を逆転
interface IServiceA {
  methodA(): void;
}

interface IServiceB {
  methodB(): void;
}

class ServiceA implements IServiceA {
  constructor(private readonly serviceB: IServiceB) {
    Object.freeze(this);
  }
  
  methodA() { 
    this.serviceB.methodB(); 
  }
}

対応するサービスBもインターフェースを実装します。

class ServiceB implements IServiceB {
  constructor(private readonly serviceA: IServiceA) {
    Object.freeze(this);
  }
  
  methodB() { 
    this.serviceA.methodA(); 
  }
}

Q: DIコンテナは必要?

A: 弊社では手動注入で十分。複雑になったらコンテナライブラリを検討。

Q: すべてのクラスでDIが必要?

A: 外部依存(DB、API、ファイルシステム)があるクラスのみ。純粋な計算クラスは不要。

Q: テストでモックが作りにくい

A: インターフェースが不十分。メソッドシグネチャを見直してインターフェースを改善。

Q: 循環依存が解決できない

A: 設計に問題がある可能性。責任を分割してクラスを再設計する。