保守性とテストしやすさのための依存性注入

保守性とテストしやすさのための依存性注入

密結合な TypeScript クラスを、給与計算、システムクロック、Amazon SES のメーラーを注入する構成へリファクタリングします。

Takahiro Iwasa
5 min read

依存性注入(Dependency Injection) は、コードを疎結合に保ち、テストしやすくする手法の一つです。小さな TypeScript の例で説明します。

密結合の例

以下の 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});
}
}

問題点は次のとおりです。

  • Salary クラスとの密結合:
    • this.salary = new Salary(this.employeeId); によって、Employee クラスと Salary クラスが直接結合しています。
    • Employee#notify のテストが実際の Salary#calculate に依存するため、異なる給与計算やエッジケースを再現しにくくなります。
  • システムクロックとの密結合:
    • const hour = (new Date()).getHours(); によって、Employee クラスがシステムクロックと結合しています。
    • 特定の時刻に対する条件分岐のテストが難しくなります。
  • AWS SES との密結合:
    • (new SES()).sendEmail(...) によって、Employee が AWS SES と直接結合しています。
    • notify をテストすると実際のメールが送信される可能性があり、単体テストには適しません。

依存性注入によるリファクタリング

依存性注入(DI) を使い、これらの依存関係を Employee から分離します。

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});
}
}

主な改善点は次のとおりです。

  • 結合度の低減:
    • SalarySystemDateEmployeeSes などのコンポーネントを、インターフェイス経由で注入します。
    • Employee クラスは特定の実装に直接依存しません。
  • テストの容易化:
    • ISalaryISystemDateIMailer のモック実装をテストに使用できます。
    • クロックや SES などの外部依存が分離されます。
  • 依存性逆転の原則
    • 上位モジュール(Employee)が、下位モジュール(SalaryDateSES)に依存しなくなります。

まとめ

リファクタリング後の Employee は、コンストラクタとメソッドを通じて ISalaryISystemDateIMailer を受け取ります。給与計算、クロック、SES メーラーをモックに差し替えてテストできます。

new Date()ISystemDate で抽象化するのは過剰に見えるかもしれませんが、時刻に依存する分岐を決定的にテストできます。「Good morning」の分岐を検証するテストが、実行時刻に左右されなくなります。

このパターンは、クラス、システムリソース、SES のような外部サービスのいずれにも適用できます。利用箇所でインターフェイスを定義し、具体的な実装を内部で生成せず、呼び出し側から渡すことがポイントです。

About the author

Takahiro Iwasa

Takahiro Iwasa

Software Developer

This blog shares technical notes from hands-on projects—architecture, implementation, and AWS service integrations.