DI(依存性注入)の基礎を理解する【Part1】〜「自分で作らない」設計でテストも拡張もラクになる〜
はじめに
- 「DIって何?よく聞くけど正直よくわからん」
- 「クラスに依存を注入って、結局何がいいの?」
- 「interface使うとなんで疎結合になるの?」
そんな疑問を持っている方に向けて、この記事では Dependency Injection(DI)=依存性の注入 について、TypeScriptで書いた最小限のコードを使ってわかりやすく解説します。
また、ソースコードを追いながらより具体的に知りたい場合は、下記のリポジトリをご覧ください
🧠 DIってなんぞや?
DIとは Dependency Injection の略で、直訳すると「依存性の注入」。
依存するオブジェクトを自分で作らず、外部から与えてもらうこと。
…と言われてもピンとこないので、まずはコードを見てみましょう。
💥 DIがないとき(依存を自分で作っている)
class Logger {
log(message: string) {
console.log(message);
}
}
class UserService {
private logger = new Logger(); // 自分で依存を生成している
createUser(name: string) {
this.logger.log(`User ${name} created`);
}
}
UserService
は Logger
の実装に直接依存しています。
これは 結合度が高く、差し替えやテストが難しい設計です。
✅ DIがあるとき(依存を外から注入)
class Logger {
log(message: string) {
console.log(message);
}
}
class UserService {
constructor(private logger: Logger) {} // 外から注入
createUser(name: string) {
this.logger.log(`User ${name} created`);
}
}
// 使用側で依存を注入
const logger = new Logger();
const userService = new UserService(logger);
userService.createUser("Alice");
このように、クラス外でインスタンスを作り、依存先に渡す設計を「依存性注入(DI)」と呼びます。
道具を自分で作るのではなく、「外部から渡してもらう」イメージです。
🔁 制御の反転(Inversion of Control)
この設計では、「依存(Logger)を誰が作るか」の制御を UserService
から 外部に移しています。
このような考え方を 制御の反転(Inversion of Control, IoC) と言い、
DIはそのIoCを 実現するための具体的な手法のひとつです。
🎁 DIを使うと何が嬉しいの?
1. 疎結合になる(実装から抽象へ)
// Loggerのinterfaceを定義
interface ILogger {
log(message: string): void;
}
// コンソール出力するLogger
class ConsoleLogger implements ILogger {
log(message: string) {
console.log(message);
}
}
// ファイルに出力するLogger(仮)
class FileLogger implements ILogger {
log(message: string) {
// ファイルに書き込む処理
}
}
// UserServiceはILoggerに依存
class UserService {
constructor(private logger: ILogger) {}
createUser(name: string) {
this.logger.log(`User ${name} created`);
}
}
// 利用側で実装を切り替えられる
const logger = new ConsoleLogger();
// const logger = new FileLogger();
const userService = new UserService(logger);
userService.createUser("Alice");
ここでは UserService
は ConsoleLogger
や FileLogger
の 具体的な実装ではなく、
ILogger という「機能の契約(interface)」にだけ依存しています。
つまり「何ができるか」には依存しているけど、「どう実現するか」には依存していない
→ これが 疎結合(loose coupling) です。
2. テストがしやすくなる
class FakeLogger implements ILogger {
log(message: string) {
// ログを記録せず、テスト用にスキップ
}
}
const testLogger = new FakeLogger();
const service = new UserService(testLogger);
// 実際のログを出力せずにテストできる!
依存先を差し替えられるため、モックやスタブによるテストが簡単に行えます。
データベースや外部APIなど副作用の大きい処理も、安全にテストできるようになります。
3. 再利用性が高くなる
interfaceに依存することで、UserServiceなどの利用者は「何を期待すべきか」が明確になります。
実装側もinterfaceを満たす限り、好きな方法で実装できるため、既存のコードに影響を与えずに新しい実装を作ることができます。
そのため、異なる用途・環境(開発、本番、テストなど)での再利用が非常にしやすくなります。
4. 依存関係が明示的になる
依存するクラスは全てコンストラクタで注入される為、どのクラスがどの依存を持っているかが明確になります。
また、外部から見た時も、インスタンス同士の関係性が分かりやすくなり、可読性が向上します。
✅ まとめ
DIを使う前 | DIを使った後 |
---|---|
クラス内で依存を生成し、結合度が高い | 依存は外部から注入、結合度が低い |
実装に依存しており、差し替え困難 | interfaceに依存し、差し替え自由 |
テストしにくい | モック注入でテストしやすい |
依存関係が不明瞭 | コンストラクタで明示的になる |
📌 補足:interfaceにするだけではDIではない
TypeScriptで ILogger
のようなinterfaceを使うことは **「疎結合のための設計」**です。
それに加えて「依存を外から注入する」という手法を取って初めて、本来のDIになります。
おわりに
- DIの基本は「使うものは自分で作らず、外からもらう」
- 依存の制御を外に出すことで、設計が柔軟でテストしやすくなる
- interfaceと組み合わせることで、本当に強力な疎結合設計が可能になる
次回は、InversifyJSを使った DIコンテナによる依存解決の自動化についてです!
次の記事はコチラ
Discussion