ポートとアダプター
アプリケーションのコアロジックを外部技術から分離し、ポート (インターフェース) とアダプター (実装) で接続するアーキテクチャ
アーキテクチャ設計
ポートとアダプターとは
ポートとアダプター (Ports and Adapters) は、Alistair Cockburn が 2005 年に提唱したヘキサゴナルアーキテクチャの別名で、アプリケーションのコアロジック (ドメイン) を外部技術 (DB、UI、外部 API) から分離する設計パターンである。
- ポート: アプリケーションが外部と通信するためのインターフェース
- アダプター: ポートの具体的な実装 (DynamoDB アダプター、SES アダプター)
コアロジックは「DynamoDB に保存する」ではなく「OrderRepository に保存する」と記述する。DynamoDB か PostgreSQL かはアダプターが決める。
構造
[HTTP Controller] [Lambda Handler] [CLI]
↓ ↓ ↓
─── 駆動側ポート (Driving Port) ───
│ │
▼ ▼
┌─────────────────────────────────────────────┐
│ アプリケーションコア │
│ ┌─────────────────────────────────────┐ │
│ │ ドメインロジック │ │
│ │ OrderService, PricingPolicy │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
│ │
─── 被駆動側ポート (Driven Port) ───
↓ ↓ ↓
[DynamoDB Adapter] [SES Adapter] [Stripe Adapter]
TypeScript での実装
// ポート (アプリケーション側が定義)
interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
interface NotificationPort {
send(userId: string, message: string): Promise<void>;
}
// アプリケーションコア (外部技術に依存しない)
class OrderService {
constructor(
private readonly repo: OrderRepository,
private readonly notifier: NotificationPort,
) {}
async completeOrder(orderId: string): Promise<void> {
const order = await this.repo.findById(orderId);
if (!order) throw new Error('Order not found');
order.complete();
await this.repo.save(order);
await this.notifier.send(order.userId, `Order ${orderId} completed`);
}
}
// アダプター (インフラ側が実装)
class DynamoDBOrderRepo implements OrderRepository {
async findById(id: string) { /* DynamoDB Query */ }
async save(order: Order) { /* DynamoDB Put */ }
}
class SESNotificationAdapter implements NotificationPort {
async send(userId: string, message: string) { /* SES SendEmail */ }
}
// 組み立て (Composition Root)
const service = new OrderService(
new DynamoDBOrderRepo(ddbClient),
new SESNotificationAdapter(sesClient),
);
駆動側と被駆動側
| 種類 | 方向 | 例 |
|---|---|---|
| 駆動側 (Driving) | 外部 → アプリ | HTTP Controller, Lambda Handler, CLI, テスト |
| 被駆動側 (Driven) | アプリ → 外部 | DB, メール, 外部 API, ファイルシステム |
駆動側アダプターはアプリケーションを「呼び出す」側、被駆動側アダプターはアプリケーションから「呼び出される」側だ。
テスタビリティ
最大のメリットはテスト時にアダプターを差し替えられること。
// テスト: インメモリアダプターを注入
const mockRepo: OrderRepository = {
findById: vi.fn().mockResolvedValue({ id: '1', userId: 'u1', complete: vi.fn() }),
save: vi.fn(),
};
const mockNotifier: NotificationPort = { send: vi.fn() };
const service = new OrderService(mockRepo, mockNotifier);
await service.completeOrder('1');
expect(mockRepo.save).toHaveBeenCalled();
expect(mockNotifier.send).toHaveBeenCalledWith('u1', expect.any(String));
DB 接続もメール送信も不要で、ミリ秒単位でテストが完了する。
Clean Architecture との関係
Clean Architecture (Robert C. Martin) はポートとアダプターの発展形で、レイヤーをより細かく分割する。本質は同じで「依存の方向を内側 (ドメイン) に向ける」ことだ。
ポートとアダプターの対応
| ポート (インターフェース) | アダプター (実装) |
|---|---|
| OrderRepository | DynamoDBOrderRepository |
| NotificationService | SESNotificationService |
| PaymentGateway | StripePaymentGateway |
| OrderRepository (テスト) | InMemoryOrderRepository |
現場での応用を知るには関連書籍も役立つ。