📥

DI(依存性注入)の基礎を理解する【Part1】〜「自分で作らない」設計でテストも拡張もラクになる〜

に公開

はじめに

  • 「DIって何?よく聞くけど正直よくわからん」
  • 「クラスに依存を注入って、結局何がいいの?」
  • 「interface使うとなんで疎結合になるの?」

そんな疑問を持っている方に向けて、この記事では Dependency Injection(DI)=依存性の注入 について、TypeScriptで書いた最小限のコードを使ってわかりやすく解説します。

また、ソースコードを追いながらより具体的に知りたい場合は、下記のリポジトリをご覧ください
https://github.com/manntera/DependencyInjectionSamples


🧠 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`);
  }
}

UserServiceLogger の実装に直接依存しています。
これは 結合度が高く、差し替えやテストが難しい設計です。


✅ 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");

ここでは UserServiceConsoleLoggerFileLogger具体的な実装ではなく、
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