依存性注入を使用して疎結合を実現する
岩佐 孝浩
6 min read
Architecting
はじめに
URL Copied!
この投稿では、Dependency Injection を用いた疎結合なコードの作成方法を解説します。初級開発者を対象に、ソフトウェア設計と保守性を向上させるための実用的なアプローチについて説明します。
疎結合とは
URL Copied!
疎結合とは、クラスやモジュールなどのソフトウェアコンポーネントが、インターフェースを介して相互作用するように設計することを指します。一方、密結合なコンポーネントは、変更やテストが困難になります。
コードを疎結合にするには追加の実装コストが発生します。プロジェクトの要件に応じて現実的なバランスを取ることが重要です。
密結合の例
URL Copied!
以下の例は、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});
}
}
密結合の問題
URL Copied!
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
メソッドをテストすると実際のメールが送信され、開発環境では実用的ではありません。
依存性注入による改善
URL Copied!
Dependency Injection (DI) を活用することで、コードのモジュール化が進み、テストがより容易になります。
リファクタリング後のコード
URL Copied!
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});
}
}
主な改善点
URL Copied!
-
結合の削減:
Salary
、SystemDate
、EmployeeSes
のようなコンポーネントは、インターフェースを通じて注入されるようになりました。Employee
クラスは特定の実装に直接依存しなくなりました。
-
テストの容易さ:
ISalary
、ISystemDate
、IMailer
のモック実装をテストに使用できます。- クロックや SES サービスのようなシステム依存が分離されました。
-
- 高水準モジュール(
Employee
)が低水準モジュール(Salary
、Date
、SES
)に依存しなくなります。
- 高水準モジュール(
まとめ
URL Copied!
依存性注入は、疎結合を促進し、コードをより柔軟でテストしやすく、保守性の高いものにします。DI の活用により、コードベースの将来の変更への適応性が向上し、堅牢なテストの実践が可能になります。
Happy Coding! 🚀