ポートとアダプター

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

アーキテクチャ設計

ポートとアダプターとは

ポートとアダプター (Ports and Adapters) は、Alistair Cockburn が 2005 年の技術レポートで示した設計パターンで、アプリケーションのコアロジック (ドメイン) を外部技術 (DB、UI、外部 API) から分離する。原典ではこの「ポートとアダプター」がパターン名で、ヘキサゴナルアーキテクチャ (Hexagonal Architecture) は図の見た目に由来する別名として併記されている。日本語圏では別名の方が先に広まったため主従が逆に見えるが、原典を読むときは正式名がこちらだと押さえておくと迷わない。原典は現在も本人のサイトで公開されている (2026 年 8 月時点)。

  • ポート: アプリケーションが外部と通信するためのインターフェース
  • アダプター: ポートの具体的な実装 (DynamoDB アダプター、SES アダプター)

コアロジックは「DynamoDB に保存する」ではなく「OrderRepository に保存する」と記述する。DynamoDB か PostgreSQL かはアダプターが決める。

構造

原典の呼称は一次 (primary) ポート・アダプターと二次 (secondary) ポート・アダプターで、駆動側 (driving) / 被駆動側 (driven) はその別称として示されている。区別の基準は「会話を始めるのがどちら側か」で、ユースケースの主アクター・副アクターの考え方に対応する。以下では日本語で通りの良い駆動側 / 被駆動側の呼び方で説明する。

駆動側ポート (Driving Port) は外部からアプリケーションコアを呼び出すインターフェースで、HTTP コントローラーや Lambda ハンドラがアダプターとして接続する。被駆動側ポート (Driven Port) はコアが外部リソースにアクセスするインターフェースで、DynamoDB や SQS のアダプターが接続する。コアはポートのインターフェースのみに依存し、具体的なアダプターを知らない。

図が六角形なのは 6 という数に意味があるからではなく、階層図の一次元的な並びから離れて必要な数のポートとアダプターを書き込めるようにするためだ。原典は実際のポート数を 2〜4 個としている (著者が見た最多が 4)。

         [HTTP Controller]  [Lambda Handler]  [CLI]
              ↓                   ↓              ↓
         ─── 駆動側ポート (Driving Port) ───
              │                                  │
              ▼                                  ▼
    ┌─────────────────────────────────────────────┐
    │           アプリケーションコア                    │
    │  ┌─────────────────────────────────────┐    │
    │  │         ドメインロジック                │    │
    │  │  OrderService, PricingPolicy        │    │
    │  └─────────────────────────────────────┘    │
    └─────────────────────────────────────────────┘
              │                                  │
         ─── 被駆動側ポート (Driven Port) ───
              ↓                   ↓              ↓
         [DynamoDB Adapter] [SES Adapter] [Stripe Adapter]

TypeScript での実装

TypeScript での実装のコード例を示す。

// ドメイン (外部技術を知らない)
interface Order {
  readonly id: string;
  readonly userId: string;
  complete(): void;
}

// ポート (アプリケーション側が定義)
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`);
  }
}

// アダプター (インフラ側が実装・SDK の import は省略)
class DynamoDBOrderRepo implements OrderRepository {
  constructor(private readonly client: DynamoDBDocumentClient) {}

  async findById(id: string): Promise<Order | null> {
    const res = await this.client.send(new GetCommand({ TableName: 'orders', Key: { id } }));
    return (res.Item as Order | undefined) ?? null;
  }

  async save(order: Order): Promise<void> {
    await this.client.send(new PutCommand({ TableName: 'orders', Item: order }));
  }
}

class SESNotificationAdapter implements NotificationPort {
  constructor(private readonly client: SESClient) {}

  async send(userId: string, message: string): Promise<void> {
    /* this.client.send(new SendEmailCommand(...)) でメール送信 */
  }
}

// 組み立て (Composition Root)
const service = new OrderService(
  new DynamoDBOrderRepo(ddbClient),
  new SESNotificationAdapter(sesClient),
);

駆動側と被駆動側

駆動側と被駆動側を以下にまとめる。

種類方向
駆動側 (Driving・原典では primary)外部 → アプリHTTP Controller, Lambda Handler, CLI, テスト
被駆動側 (Driven・原典では secondary)アプリ → 外部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) は 2012 年の記事で、ヘキサゴナル (ポートとアダプター)・オニオンアーキテクチャ・DCI・BCE といった先行案を「目的は同じ」と整理し、1 枚の同心円図に統合したものだ。ポートとアダプターの直系の後継ではなく、同じ狙いを持つ複数の案の共通項を言い直した位置づけになる。共通の核は「依存の方向を内側 (ドメイン) に向ける」ことで、Clean Architecture はそこにレイヤーの細分と依存性逆転の作法を足している。

ポートとアダプターの対応

ポートとアダプターの対応を以下に整理する。

ポート (インターフェース)アダプター (実装)
OrderRepositoryDynamoDBOrderRepository
NotificationServiceSESNotificationService
PaymentGatewayStripePaymentGateway
OrderRepository (テスト)InMemoryOrderRepository

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

この記事は役に立ちましたか?

関連用語

関連する記事