依存性注入を利用したコードの疎結合化
この投稿では、依存性注入 (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#notify
で Salary#calculate
をテストする場合、 undefined
が返されるケースをテストすることが難しくなるかもしれません。
システムクロックと密結合
const hour = (new Date()).getHours();
の部分は、 Employee
クラスとシステムクロックの間で強い結合を作り出します。
Employee#notify
で if
をテストすることが困難になります。
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 を利用すると、コードの結合度が低下することで変更に強くなり、テストが容易になります。
この投稿が、お役に立てば幸いです。