依存性注入(DI)入門 - 疎結合な設計を実現する基礎技法

16分 で読める | 2026.04.10

公式ドキュメント

依存性注入(Dependency Injection、DI)は、classが必要なobjectを内部で作らず、外から受け取る設計です。

この記事では、注文を保存する小さなTypeScript codeを使い、newで固定された依存先をconstructor injectionへ変えます。最後にfakeを渡してtestし、applicationの入口で本物を組み立てます。

まず結論

次の2つを比べてください。

class OrderService {
  private repository = new PostgresOrderRepository();
}
class OrderService {
  constructor(
    private readonly repository: OrderRepository,
  ) {}
}

上はOrderService自身がPostgreSQL用repositoryを作っています。下は必要なrepositoryを外から受け取ります。

この小さな違いにより、本番ではPostgreSQL実装、testではmemory上のfakeを渡せます。

依存先を内部でnewして固定する設計と、composition rootから本番実装やfakeを外から渡すDIを比較する図

「依存する」とは

ある処理が別のobjectなしでは仕事を完了できない時、そのobjectへ依存しています。

注文を登録するserviceなら、次へ依存するかもしれません。

  • 注文を保存するrepository
  • 完了mailを送るmailer
  • 時刻を返すclock

依存があること自体は問題ではありません。問題は、業務処理が依存先の作り方まで知り、交換できなくなることです。

newで固定すると何が困るか

最初の実装を見ます。

type Order = {
  id: string;
  total: number;
};

class PostgresOrderRepository {
  async save(order: Order): Promise<void> {
    console.log("save to PostgreSQL", order);
  }
}

class OrderService {
  private readonly repository =
    new PostgresOrderRepository();

  async place(order: Order): Promise<void> {
    if (order.total <= 0) {
      throw new Error("total must be positive");
    }

    await this.repository.save(order);
  }
}

place()の仕事は、注文を検証して保存することです。しかしclass内部のnewにより、次の責任まで持っています。

  • どのrepository実装を使うか
  • 依存先をいつ作るか
  • 依存先の設定をどう渡すか

testでplace()だけを確かめたい時にも、PostgreSQL実装が付いてきます。

constructor injectionへ変える

まず、serviceが必要とする最小の操作をinterfaceにします。

type Order = {
  id: string;
  total: number;
};

interface OrderRepository {
  save(order: Order): Promise<void>;
}

serviceはinterfaceをconstructorで受け取ります。

class OrderService {
  constructor(
    private readonly repository: OrderRepository,
  ) {}

  async place(order: Order): Promise<void> {
    if (order.total <= 0) {
      throw new Error("total must be positive");
    }

    await this.repository.save(order);
  }
}

OrderServiceは「save()できるobject」が必要だと宣言します。PostgreSQLへの接続方法は知りません。

constructor injectionには次の利点があります。

  • 必要な依存がconstructorから分かる
  • 依存なしの不完全なinstanceを作りにくい
  • propertyを後から差し替えずに済む
  • test用実装を渡しやすい

この記事では、依存が必須ならconstructor injectionを基本にします。

本番用repositoryを作る

interfaceを満たす実装を用意します。

class PostgresOrderRepository
  implements OrderRepository
{
  async save(order: Order): Promise<void> {
    console.log("save to PostgreSQL", order);
  }
}

TypeScriptのinterfaceは実行時には消えます。重要なのは、serviceと実装が同じmethod契約を共有することです。

実際のprojectでは、このclassがdatabase clientを受け取り、SQLやORMを呼びます。業務規則をrepositoryへ移す必要はありません。

fakeを使ってtestする

databaseへ接続せず、保存された注文をmemoryへ記録するfakeを作ります。

class FakeOrderRepository
  implements OrderRepository
{
  readonly saved: Order[] = [];

  async save(order: Order): Promise<void> {
    this.saved.push(order);
  }
}

Node.js標準test runnerでserviceを確認します。

import assert from "node:assert/strict";
import test from "node:test";

test("有効な注文をrepositoryへ保存する", async () => {
  const repository = new FakeOrderRepository();
  const service = new OrderService(repository);
  const order = { id: "order-1", total: 3000 };

  await service.place(order);

  assert.deepEqual(repository.saved, [order]);
});

test("0円の注文を保存しない", async () => {
  const repository = new FakeOrderRepository();
  const service = new OrderService(repository);

  await assert.rejects(
    () => service.place({ id: "order-2", total: 0 }),
    /total must be positive/,
  );

  assert.deepEqual(repository.saved, []);
});

このtestが確認しているのはOrderServiceの規則です。database接続testはPostgresOrderRepository側へ分けられます。

fake、stub、mockの違い

test doubleには複数の呼び方があります。

名前主な役割
fake簡単に動く代替実装
stub決めた値を返す
spy呼び出しを記録する
mock期待した呼び出しか検証する

FakeOrderRepositoryはmemory上で保存動作を実装しているためfakeです。同時にstored配列で呼び出し結果も観測できます。

名前を暗記することより、何を置き換え、何を確認するかを明確にする方が重要です。

composition rootで組み立てる

依存先を外へ出すと、どこかでobjectを組み立てる必要があります。その場所をcomposition rootと呼びます。

async function main() {
  const repository =
    new PostgresOrderRepository();
  const service =
    new OrderService(repository);

  await service.place({
    id: "order-1",
    total: 3000,
  });
}

await main();

composition rootはapplicationの入口付近へ置きます。

  • CLIならmain.ts
  • Web serverなら起動file
  • serverlessならhandlerを作るmodule

業務classの各所でばらばらに依存を組み立てると、交換箇所が再び増えます。

containerは必須ではない

DI containerは、objectの登録と組み立てを自動化するtoolです。DIそのものではありません。

小さなapplicationなら、先ほどのようにnewを数行書く方が分かりやすい場合があります。この方法はPure DIとも呼ばれます。

containerを導入する前に、次を確認します。

  • 手作業の組み立てが本当に複雑か
  • objectのlifetime管理が必要か
  • teamがcontainerの規則を理解できるか
  • error時に組み立てを追跡できるか

「DIを使うならcontainerが必要」という理解は誤りです。

DI、IoC、DIPを区別する

似た用語を短く整理します。

DI

必要な依存objectを外から渡す具体的な設計手法です。今回のconstructor injectionが該当します。

IoC

Inversion of Controlは、処理や制御の決定を自分のcodeだけで握らず、外部の仕組みへ移す広い考え方です。DIはIoCを実現する一つの方法です。

DIP

Dependency Inversion Principleは、上位の方針を下位の具体的実装へ直接結び付けず、両方を抽象へ依存させる設計原則です。

今回なら、業務規則を持つOrderServiceとPostgreSQL実装がOrderRepositoryという契約で分かれています。

用語の関係は次のように考えられます。

DIP: 依存方向を考える原則
DI : 依存を外から渡す手法
IoC: 制御を外へ移す広い考え方

三つは同じ意味ではありません。

interfaceを増やしすぎない

DIを学ぶと、すべてのclassへinterfaceを作りたくなることがあります。

次のような小さく安定した値計算まで、必ず抽象化する必要はありません。

function addTax(price: number): number {
  return Math.floor(price * 1.1);
}

交換する必要がある境界、外部I/O、testで制御したい依存から検討します。

  • database
  • mail service
  • external API
  • 現在時刻
  • random値
  • file system

interfaceが1つの実装をそのまま写しただけで、交換もtestも不要なら、抽象化が早すぎる可能性があります。

よくある誤解

newはすべて禁止

newは必要です。問題は業務処理の内部に組み立てが散らばることです。composition rootでは具体objectをnewします。

fake testがあれば本番接続testは不要

fakeは業務規則を速く確認できますが、本物のdatabaseとの契約までは保証しません。repositoryのintegration testは別に必要です。

constructorが長いほどDIができている

依存が多すぎるconstructorは、classの責任が広すぎる合図かもしれません。containerで隠す前に責任を見直します。

service locatorも同じ

method内部からglobal containerを呼び出すservice locatorでは、必要な依存がconstructorから見えません。この記事のDIとは区別します。

導入するか判断する質問

次の質問へ複数当てはまるならDIが役立ちます。

  • 外部serviceやdatabaseを交換したいか
  • testで失敗、時刻、返り値を制御したいか
  • 業務規則とI/Oを分けたいか
  • environmentごとに実装を変えるか
  • 依存先の構築設定が業務classへ漏れているか

単純なscriptで依存先が1つしかなく、testでも交換しないなら、直接呼び出す方が簡潔な場合もあります。

まとめ

  • DIは依存objectを外から渡す手法
  • constructor injectionは必須依存を明示しやすい
  • fakeを渡すと業務規則を速くtestできる
  • 具体実装はcomposition rootで組み立てる
  • DI containerは任意であり、小規模なら不要
  • DI、IoC、DIPは関係するが同じ意味ではない

参考リソース

← 一覧に戻る
PR
PR
PR
PR