1自分で new する場合と外から渡す場合
依存を自分で new するクラスと、外から受け取るクラスを並べた最小比較です。注入される側のコードは1文字も変えずにロガーを差し替えられる、という DI の利点が最小単位で分かります。
TypeScript
2契約(型)に依存して実装を選ぶ
利用側が具体的なクラス名ではなく「ログを書ける何か」という契約だけに依存する形です。開発中はコンソール、本番はファイル、のように組み立て時に実装を選べるのが、DI と相性の良い設計の核心です。
// 利用側は「ログを書ける何か」という契約(型)だけに依存する
type Logger = { log(message: string): void };
class ConsoleLogger {
log(message: string): void {
console.log(message);
}
}
class FileLogger {
log(message: string): void {
/* ファイルに追記する処理 */
}
}
class OrderService {
constructor(private logger: Logger) {} // どちらが来ても動く
placeOrder(): void {
this.logger.log("注文を受け付けました");
}
}
// 開発: new OrderService(new ConsoleLogger())
// 本番: new OrderService(new FileLogger())
3テストでの差し替え — 偽物のメール送信
DI の実務上いちばん大きな見返りであるテスト容易性の例です。本物のメール送信クラスを「記録するだけの偽物」に差し替えることで、メールを実際に送らずに送信内容を検証できます。
TypeScript
4依存グラフの手動組み立て
実務アプリで典型的な「コントローラ → サービス → リポジトリ → DB接続」という多段の依存を手動で組み立てた例です。末端から順に new する必要があり、この面倒さを自動化するのが DI コンテナの仕事だと分かります。
// 実務アプリの依存関係は多段になる
// OrdersController → OrdersService → OrderRepository → DbConnection
class DbConnection {}
class OrderRepository {
constructor(private db: DbConnection) {}
}
class OrdersService {
constructor(private repo: OrderRepository) {}
}
class OrdersController {
constructor(private service: OrdersService) {}
}
// 手動だと末端から順に new する必要がある(依存が増えるほど悲惨)
const controller = new OrdersController(
new OrdersService(new OrderRepository(new DbConnection())),
);
// この組み立て作業を肩代わりするのが DI コンテナ
5NestJSのコンテナが肩代わりすること
手動の組み立てと NestJS に任せた場合の対比を整理したまとめです。開発者の仕事は「欲しい型をコンストラクタに書く」ことだけになる、という次のレッスン以降の世界観を予告します。
// 前: 手動の組み立て(どこかに必ず new の山ができる)
// const controller = new OrdersController(
// new OrdersService(new OrderRepository(new DbConnection())),
// );
//
// 後: NestJS の DI コンテナに任せた世界
// - 登録 … クラスをコンテナに登録する(書き方は次のレッスン)
// - 解決 … コンストラクタの型を見て必要な部品を自動で渡す
// - 寿命 … 一度作ったインスタンスはアプリ全体で使い回す
//
// 開発者がやるのは
// constructor(private service: OrdersService) {}
// と「欲しい型」を宣言することだけになる