LESSON 30分

ストーリー

高橋アーキテクト
Use CaseがPortに依存し、AdapterがPortを実装する。この仕組みは理解できたと思う
あなた
でも、実行時に”どのAdapter”を使うかは誰が決めるんですか?
高橋アーキテクト
良い質問だ。それが依存性注入 — Dependency Injection の出番だ。DIはアーキテクチャの全体を組み立てる”接着剤”のような存在だ

DIの3つの方法

1. コンストラクタインジェクション(推奨)

// Use Caseは「何が注入されるか」を知らない
class CreateOrderUseCase {
  constructor(
    private readonly orderRepo: OrderRepository,    // インターフェース
    private readonly paymentGw: PaymentGateway,      // インターフェース
    private readonly notifier: NotificationSender    // インターフェース
  ) {}

  async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
    // orderRepoがPrismaなのかInMemoryなのかを知らない
    const order = Order.create(input.customerId, input.items);
    await this.orderRepo.save(order);
    return { orderId: order.id.value, status: order.status };
  }
}

2. セッターインジェクション

class CreateOrderUseCase {
  private orderRepo!: OrderRepository;

  setOrderRepository(repo: OrderRepository): void {
    this.orderRepo = repo;
  }
  // 注入忘れのリスクがある → コンストラクタインジェクションを推奨
}

3. インターフェースインジェクション

interface OrderRepositoryAware {
  setOrderRepository(repo: OrderRepository): void;
}

class CreateOrderUseCase implements OrderRepositoryAware {
  private orderRepo!: OrderRepository;

  setOrderRepository(repo: OrderRepository): void {
    this.orderRepo = repo;
  }
}

Composition Root(組み立て地点)

全ての依存関係を組み立てる場所をComposition Rootと呼びます。アプリケーションのエントリーポイント付近に配置します。

// composition-root.ts(アプリケーション起動時に1回だけ実行)
import { PrismaClient } from '@prisma/client';

function createContainer() {
  // Layer 4: フレームワーク・ドライバー
  const prisma = new PrismaClient();
  const stripeApiKey = process.env.STRIPE_API_KEY!;

  // Layer 3: Driven Adapters(出力アダプター)
  const orderRepo = new PrismaOrderRepository(prisma);
  const paymentGw = new StripePaymentGateway(stripeApiKey);
  const notifier = new SendGridNotificationSender(process.env.SENDGRID_KEY!);
  const inventoryChecker = new HttpInventoryChecker(process.env.INVENTORY_URL!);

  // Layer 2: Use Cases
  const createOrder = new CreateOrderUseCase(
    orderRepo, inventoryChecker, paymentGw, notifier
  );
  const cancelOrder = new CancelOrderUseCase(orderRepo, paymentGw);
  const getOrderDetail = new GetOrderDetailUseCase(orderRepo);

  // Layer 3: Driving Adapters(入力アダプター)
  const orderController = new OrderController(
    createOrder, cancelOrder, getOrderDetail
  );

  return { orderController, prisma };
}

// main.ts
const container = createContainer();
const app = express();
app.post('/api/orders', (req, res) => container.orderController.create(req, res));
app.delete('/api/orders/:id', (req, res) => container.orderController.cancel(req, res));
app.get('/api/orders/:id', (req, res) => container.orderController.getById(req, res));
app.listen(3000);

テスト用のComposition Root

// test/helpers/createTestContainer.ts
function createTestContainer() {
  // テスト用のInMemory/Stub Adapterを注入
  const orderRepo = new InMemoryOrderRepository();
  const paymentGw = new StubPaymentGateway();
  const notifier = new SpyNotificationSender();
  const inventoryChecker = new StubInventoryChecker();

  const createOrder = new CreateOrderUseCase(
    orderRepo, inventoryChecker, paymentGw, notifier
  );

  return {
    createOrder,
    orderRepo,        // テストで状態を確認するために公開
    paymentGw,         // Stubの動作を制御するために公開
    notifier,          // 呼び出しを検証するために公開
    inventoryChecker,
  };
}

// test/use-cases/CreateOrderUseCase.test.ts
describe('CreateOrderUseCase', () => {
  it('正常に注文を作成できる', async () => {
    const container = createTestContainer();
    container.inventoryChecker.setAvailable(true);
    container.paymentGw.setSuccess(true);

    const output = await container.createOrder.execute({
      customerId: 'c-1',
      items: [{ productId: 'p-1', quantity: 1 }],
      paymentMethod: { type: 'credit_card', token: 'tok' },
      shippingAddress: '東京都',
    });

    expect(output.orderId).toBeDefined();
    expect(container.notifier.wasCalled()).toBe(true);
  });
});

DIコンテナライブラリ(応用)

大規模プロジェクトでは、手動の組み立てが煩雑になります。DIコンテナライブラリを使うと自動化できます。

// tsyringeを使った例
import { container, injectable, inject } from 'tsyringe';

@injectable()
class CreateOrderUseCase {
  constructor(
    @inject('OrderRepository') private orderRepo: OrderRepository,
    @inject('PaymentGateway') private paymentGw: PaymentGateway
  ) {}
}

// 登録
container.register('OrderRepository', { useClass: PrismaOrderRepository });
container.register('PaymentGateway', { useClass: StripePaymentGateway });

// 解決(自動的に依存が注入される)
const useCase = container.resolve(CreateOrderUseCase);

ただし、Month1で学んだSOLID原則を理解しているなら、手動DIでも十分に管理できます。DIコンテナは便利ですがフレームワーク依存が増えるトレードオフがあります。


DIの設計原則

原則説明
コンストラクタインジェクション推奨依存が明示的で、注入忘れがコンパイル時に検出される
Composition Root依存の組み立ては1箇所に集約する
環境別切り替え本番/テスト/開発で異なるAdapterを注入
インターフェースに依存具象クラスではなくインターフェースに依存する

まとめ

ポイント内容
DI外部からインターフェースの実装を注入する仕組み
コンストラクタインジェクション最も推奨される注入方法
Composition Root全ての依存を組み立てるエントリーポイント
テストテスト用Composition Rootで差し替え
DIコンテナ大規模プロジェクトでは自動化が有効

チェックリスト

  • 3つのDI方法を説明できる
  • Composition Rootの役割を理解した
  • テスト用のAdapterに差し替える方法を理解した
  • コンストラクタインジェクションが推奨される理由を説明できる

次のステップへ

次は「テスタビリティの確保」を学びます。DIを活用して、各層のテストをどのように設計するかを具体的に見ていきましょう。


推定読了時間: 30分