依存性注入(Dependency Injection、通称DI)について
依存性注入(Dependency Injection、通称DI) は、ソフトウェアの設計パターンの一つです。
クラスが必要とする依存関係(オブジェクトやサービス)を外部から注入する仕組みです。
-
依存性注入の目的
- テストのしやすさ: クラスが依存する外部サービスをモックに置き換えて、テストしやすくなります。
- コードの柔軟性: 依存する実装を簡単に差し替えたり、複数の実装を持たせることができます。
-
依存性注入の種類
- コンストラクタインジェクション: 依存性をコンストラクタで渡す方法。
- セッターインジェクション: セッターメソッドで依存性を渡す方法。
- インターフェースインジェクション: インターフェースを介して依存性を注入する方法。
1. コンストラクタインジェクション(Constructor Injection)
特徴:
依存性が必須であることを保証できる: オブジェクトがインスタンス化される時点で、すべての依存関係が提供されるため、クラスが動作に必要とする依存性が漏れなく注入されることを保証できます。
クラスの設計が明確になる: 依存性がコンストラクタに集約されることで、クラスがどのような外部サービスやオブジェクトに依存しているかが一目でわかります。これにより、クラスの責任範囲が明確になり、クラスの役割や設計がより理解しやすくなります。
ステップ1: インターフェースの定義
ログを出力するLogger
インターフェースを定義します。
interface Logger {
log(message: string): void;
}
ステップ2: 具体的な実装クラス
このLogger
インターフェースを実装するクラスを作成します。
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`ConsoleLogger: ${message}`);
}
}
class FileLogger implements Logger {
log(message: string): void {
// ファイルにログを書き込む処理(簡略化)
console.log(`FileLogger: ${message}`);
}
}
ステップ3: 依存性を注入するクラス
このLogger
インターフェースに依存するUserService
クラスを作成します。依存性注入を使ってLogger
の具体的な実装を外部から渡すようにします。
class UserService {
private logger: Logger;
// コンストラクタインジェクション
constructor(logger: Logger) {
this.logger = logger;
}
createUser(username: string): void {
// ユーザー作成処理(簡略化)
this.logger.log(`User created: ${username}`);
}
}
ステップ4: 依存性の注入と利用
UserService
にLogger
を注入して利用します。
const consoleLogger = new ConsoleLogger();
const userServiceWithConsoleLogger = new UserService(consoleLogger);
userServiceWithConsoleLogger.createUser('Alice');
// 結果: ConsoleLogger: User created: Alice
const fileLogger = new FileLogger();
const userServiceWithFileLogger = new UserService(fileLogger);
userServiceWithFileLogger.createUser('Bob');
// 結果: FileLogger: User created: Bob
2. セッターインジェクション(Setter Injection)
特徴:
可変性: コンストラクタでなく、セッターメソッド(setter method)を通じて依存性を設定するため、依存性を後から変更することが可能。
遅延注入: クラスのインスタンス生成後に依存性を設定できる。
// ステップ1: インターフェースの定義
interface Logger {
log(message: string): void;
}
// ステップ2: 具体的な実装クラス
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`ConsoleLogger: ${message}`);
}
}
// ステップ3: 依存性を注入するクラス
class UserService {
private logger?: Logger;
// セッターインジェクション
public setLogger(logger: Logger): void {
this.logger = logger;
}
public createUser(username: string): void {
if (this.logger) {
this.logger.log(`User created: ${username}`);
} else {
console.log(`No logger set for UserService`);
}
}
}
利用例
const userService = new UserService();
// まだLoggerが設定されていない状態でユーザー作成
userService.createUser('Alice');
// 出力: No logger set for UserService
// セッターで依存性(Logger)を注入
const consoleLogger = new ConsoleLogger();
userService.setLogger(consoleLogger);
// ロガーを使ってユーザー作成
userService.createUser('Bob');
// 出力: ConsoleLogger: User created: Bob
3. インターフェースインジェクション(Interface Injection)
特徴:
明確な契約: 依存性を注入するインターフェースが明確に定義され、クラスはそのインターフェースに基づいて依存性を受け取ります。
疎結合: 依存関係がより明確に分離され、インターフェースに従って依存性を注入することができます。
// ステップ1: インターフェースの定義
interface Logger {
log(message: string): void;
}
// インターフェースインジェクション用のインターフェース
interface InjectableLogger {
setLogger(logger: Logger): void;
}
// ステップ2: 具体的な実装クラス
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`ConsoleLogger: ${message}`);
}
}
// ステップ3: 依存性を注入するクラス
class UserService implements InjectableLogger {
private logger?: Logger;
// インターフェースインジェクションに基づくセッター
public setLogger(logger: Logger): void {
this.logger = logger;
}
public createUser(username: string): void {
if (this.logger) {
this.logger.log(`User created: ${username}`);
} else {
console.log(`No logger set for UserService`);
}
}
}
利用例
const userService = new UserService();
const consoleLogger = new ConsoleLogger();
// インターフェース経由で依存性を注入
userService.setLogger(consoleLogger);
userService.createUser('Charlie');
// 出力: ConsoleLogger: User created: Charlie