ストーリー
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分