保守性とテストしやすさのための依存性注入
密結合な TypeScript クラスを、給与計算、システムクロック、Amazon SES のメーラーを注入する構成へリファクタリングします。
依存性注入(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}); }}主な改善点は次のとおりです。
- 結合度の低減:
Salary、SystemDate、EmployeeSesなどのコンポーネントを、インターフェイス経由で注入します。Employeeクラスは特定の実装に直接依存しません。
- テストの容易化:
ISalary、ISystemDate、IMailerのモック実装をテストに使用できます。- クロックや SES などの外部依存が分離されます。
- 依存性逆転の原則:
- 上位モジュール(
Employee)が、下位モジュール(Salary、Date、SES)に依存しなくなります。
- 上位モジュール(
まとめ
リファクタリング後の Employee は、コンストラクタとメソッドを通じて ISalary、ISystemDate、IMailer を受け取ります。給与計算、クロック、SES メーラーをモックに差し替えてテストできます。
new Date() を ISystemDate で抽象化するのは過剰に見えるかもしれませんが、時刻に依存する分岐を決定的にテストできます。「Good morning」の分岐を検証するテストが、実行時刻に左右されなくなります。
このパターンは、クラス、システムリソース、SES のような外部サービスのいずれにも適用できます。利用箇所でインターフェイスを定義し、具体的な実装を内部で生成せず、呼び出し側から渡すことがポイントです。
Related posts
Python で OPC UA の変数ノードを監視する
opcua-asyncio を使って OPC UA の変数ノードを購読し、ポーリングではなくサーバーからの通知で変更を処理します。
Tesseract と Pytesseract による日本語 PDF の OCR 処理
Tesseract OCR v4 と pytesseract を使って PDF から日本語テキストを抽出し、出力を整えるための正規化処理も行います。
インターネット接続なしで Python パッケージをインストールする
接続可能なマシンで Python パッケージをダウンロードして転送し、インターネットに接続できない環境へインストールする方法を解説します。
Jasmine でモックオブジェクトのプロパティをスパイする
モックオブジェクトのプロパティをスパイする際に発生する Jasmine の "already been spied upon" エラーを、Object.getOwnPropertyDescriptor で回避する方法を解説します。
PhpStorm と Xdebug で AWS EC2 上の PHP をリモートデバッグする
PhpStorm と Xdebug を使って EC2 上の PHP アプリケーションをリモートデバッグする方法を、サーバー側の php.ini の設定から IDE のパスマッピングまで解説します。
