ポートとアダプター

アプリケーションのコアロジックを外部技術から分離し、ポート (インターフェース) とアダプター (実装) で接続するアーキテクチャ

アーキテクチャ設計

ポートとアダプターとは

ポートとアダプター (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

現場での応用を知るには関連書籍も役立つ。

関連用語