依存性注入を利用したコードの疎結合化

依存性注入を利用したコードの疎結合化

Takahiro Iwasa
Takahiro Iwasa
5 min read
Architecting

この投稿では、依存性注入 (Dependency Injection) を使用してコードを疎結合にする方法を説明します。 これはもともと、職場の若手開発者向けに書いたものです。

疎結合

ソフトウェア構成要素(クラス、モジュール、ライブラリ、クラウドプラットフォームなど)は、インターフェースを介して疎結合になっているほうが良いです。 密結合の要素は、変更やテストが難しいことがよくあります。

密結合の例

下の例は、 TypeScript を利用した従業員管理機能です。

export class Salary {
  readonly employeeId: number;

  constructor(employeeId: number) {
    this.employeeId = employeeId;
  }

  calculate(): number {
    let salary = 0;
    // ...
    salary = 200000;
    return salary;
  }
}

export class Employee {
  private employeeId: number;
  private name: string;
  private salary: Salary;

  constructor(employeeId: number, name: string) {
    this.employeeId = employeeId;
    this.name = name;
    this.salary = new Salary(this.employeeId);
  }

  // Send an email by Amazon SES. Message text depends on time.
  notify(): void {
    const hour = (new Date()).getHours();
    let title = `Hi ${this.name}`;
    const body = `Current Salary: ${this.salary.calculate()}`;

    if (6 <= hour && hour <= 9) {
      title = `Good morning ${this.name}`;
    } else if (10 <= hour && hour <= 18) {
      title = `How's it going, ${this.name}?`;
    }
    (new SES()).sendEmail({title: title, body: body});
  }
}

分析

上記のサンプルコードでは、 new キーワードを使用してインスタンスを作成しています。 これにより、コンポーネント間の結合が強くなります。

Salary クラスと密結合

this.salary = new Salary(this.employeeId); の部分は、 Employee クラスと Salary クラスの間で強い結合を作り出します。 Employee#notifySalary#calculate をテストする場合、 undefined が返されるケースをテストすることが難しくなるかもしれません。

システムクロックと密結合

const hour = (new Date()).getHours(); の部分は、 Employee クラスとシステムクロックの間で強い結合を作り出します。 Employee#notifyif をテストすることが困難になります。

AWS と密結合

(new SES()).sendEmail({title: title, body: body}); の部分は、 Employee クラスと Amazon SES の間で強い結合を作り出します。 Employee#notify のテストで常に実際のメールを送信してしまいます。 また、開発環境で AWS を使用できない可能性もあります。

分解アプローチ

密結合のコンポーネントを分解するための解決策の1つとして、依存性注入を使用できます。

疎結合の例

依存性注入を利用してサンプルのコードを分解してみます。

export interface ISalary {
  readonly employeeId: number;
  calculate(): number;
}

export interface ISystemDate {
  now(): Date;
}

export interface IMailer {
  send(config: any): void;
}

export class Salary implements ISalary {
  readonly employeeId: number;

  constructor(employeeId: number) {
    this.employeeId = employeeId;
  }

  calculate(): number {
    let salary = 0;
    // ...
    salary = 200000;
    return salary;
  }
}

export class SystemDate implements ISystemDate {
  now(): Date {
    return new Date();
  }
}

export class EmployeeSes implements IMailer {
  send(config: any): void {
    (new SES()).sendEmail(config);
  }
}

export class Employee {
  private employeeId: number;
  private name: string;
  private salary: ISalary;

  constructor(employeeId: number, name: string, salary: ISalary) {
    this.employeeId = employeeId;
    this.name = name;
    this.salary = salary;
  }

  // Send an email by Amazon SES. Message text depends on time.
  notify(systemDate: ISystemDate, mailer: IMailer): void {
    const hour = systemDate.now().getHours();
    let title = `Hi ${this.name}`;
    const body = `Current Salary: ${this.salary.calculate()}`;

    if (6 <= hour && hour <= 9) {
      title = `Good morning ${this.name}`;
    } else if (10 <= hour && hour <= 18) {
      title = `How's it going, ${this.name}?`;
    }
    mailer.send({title: title, body: body});
  }
}

依存性注入を使用することで、コードを疎結合にすることができました。 さらに、依存関係の方向が反転し、 Employee クラスが直接 Salary, Date, SES クラスに依存しないようになりました。 このアプローチは一般的に依存性逆転の原則 (Dependency Inversion Principle: DIP) として知られています。

まとめ

DI を利用すると、コードの結合度が低下することで変更に強くなり、テストが容易になります。

この投稿が、お役に立てば幸いです。

Takahiro Iwasa

Takahiro Iwasa

Software Developer at KAKEHASHI Inc.
Involved in the requirements definition, design, and development of cloud-native applications using AWS. Now, building a new prescription data collection platform at KAKEHASHI Inc. Japan AWS Top Engineers 2020-2023.