hexagonal-architecture

hexagonal-architecture

熱門

設計、實作與重構 Ports & Adapters 系統,具備清晰的領域邊界、依賴反轉,以及可測試的用例編排,支援 TypeScript、Java、Kotlin 與 Go 服務。

23萬星標
3.5萬分支
更新於 2026/7/17
SKILL.md
readonlyread-only
name
hexagonal-architecture
description

Design, implement, and refactor Ports & Adapters systems with clear domain boundaries, dependency inversion, and testable use-case orchestration across TypeScript, Java, Kotlin, and Go services.

六角形架構

六角形架構(Ports and Adapters)讓商業邏輯獨立於框架、傳輸層與持久化細節。核心應用依賴抽象埠,而適配器則在邊界實作這些埠。

使用時機

  • 建立新功能,且長期可維護性與可測試性至關重要。
  • 重構分層式或框架過重的程式碼,其中領域邏輯與 I/O 關注點混雜。
  • 支援同一個用例的多種介面(HTTP、CLI、佇列工作者、排程任務)。
  • 更換基礎設施(資料庫、外部 API、訊息佇列)而不需重寫商業規則。

當請求涉及邊界、領域驅動設計、重構緊耦合服務,或將應用邏輯與特定函式庫解耦時,請使用此技能。

核心概念

  • 領域模型:商業規則與實體/值物件。不匯入框架。
  • 用例(應用層):編排領域行為與工作流程步驟。
  • 輸入埠:描述應用能做什麼的合約(命令/查詢/用例介面)。
  • 輸出埠:應用所需依賴項的合約(儲存庫、閘道、事件發布器、時鐘、UUID 等)。
  • 適配器:埠的基礎設施與傳遞實作(HTTP 控制器、資料庫儲存庫、佇列消費者、SDK 包裝器)。
  • 組合根:單一接線位置,將具體適配器綁定到用例。

輸出埠介面通常位於應用層(或僅在領域層,當抽象確實是領域層級時),而基礎設施適配器則實作它們。

依賴方向始終向內:

  • 適配器 -> 應用/領域
  • 應用 -> 埠介面(輸入/輸出合約)
  • 領域 -> 僅領域層級的抽象(無框架或基礎設施依賴)
  • 領域 -> 不依賴任何外部

運作方式

步驟 1:建模用例邊界

定義單一用例,具有明確的輸入與輸出 DTO。將傳輸細節(Express req、GraphQL context、任務負載包裝器)保留在此邊界之外。

步驟 2:先定義輸出埠

將每個副作用識別為一個埠:

  • 持久化(UserRepositoryPort
  • 外部呼叫(BillingGatewayPort
  • 橫切關注點(LoggerPortClockPort

埠應建模能力,而非技術。

步驟 3:以純編排實作用例

用例類別/函式透過建構子/引數接收埠。它驗證應用層級的不變條件,協調領域規則,並回傳純資料結構。

步驟 4:在邊界建立適配器

  • 輸入適配器將協定輸入轉換為用例輸入。
  • 輸出適配器將應用合約對應到具體 API/ORM/查詢建構器。
  • 對應邏輯留在適配器中,而非用例內部。

步驟 5:在組合根中接線所有元件

實例化適配器,然後注入到用例中。保持此接線集中化,以避免隱藏的服務定位器行為。

步驟 6:按邊界測試

  • 使用假埠對用例進行單元測試。
  • 使用真實基礎設施依賴對適配器進行整合測試。
  • 透過輸入適配器對使用者面向流程進行端到端測試。

架構圖

flowchart LR
  Client["Client (HTTP/CLI/Worker)"] --> InboundAdapter["Inbound Adapter"]
  InboundAdapter -->|"calls"| UseCase["UseCase (Application Layer)"]
  UseCase -->|"uses"| OutboundPort["OutboundPort (Interface)"]
  OutboundAdapter["Outbound Adapter"] -->|"implements"| OutboundPort
  OutboundAdapter --> ExternalSystem["DB/API/Queue"]
  UseCase --> DomainModel["DomainModel"]

建議的模組佈局

使用以功能優先的組織方式,並帶有明確的邊界:

src/
  features/
    orders/
      domain/
        Order.ts
        OrderPolicy.ts
      application/
        ports/
          inbound/
            CreateOrder.ts
          outbound/
            OrderRepositoryPort.ts
            PaymentGatewayPort.ts
        use-cases/
          CreateOrderUseCase.ts
      adapters/
        inbound/
          http/
            createOrderRoute.ts
        outbound/
          postgres/
            PostgresOrderRepository.ts
          stripe/
            StripePaymentGateway.ts
      composition/
        ordersContainer.ts

TypeScript 範例

埠定義

export interface OrderRepositoryPort {
  save(order: Order): Promise<void>;
  findById(orderId: string): Promise<Order | null>;
}

export interface PaymentGatewayPort {
  authorize(input: { orderId: string; amountCents: number }): Promise<{ authorizationId: string }>;
}

用例

type CreateOrderInput = {
  orderId: string;
  amountCents: number;
};

type CreateOrderOutput = {
  orderId: string;
  authorizationId: string;
};

export class CreateOrderUseCase {
  constructor(
    private readonly orderRepository: OrderRepositoryPort,
    private readonly paymentGateway: PaymentGatewayPort
  ) {}

  async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
    const order = Order.create({ id: input.orderId, amountCents: input.amountCents });

    const auth = await this.paymentGateway.authorize({
      orderId: order.id,
      amountCents: order.amountCents,
    });

    // markAuthorized 回傳新的 Order 實例;不會原地修改。
    const authorizedOrder = order.markAuthorized(auth.authorizationId);
    await this.orderRepository.save(authorizedOrder);

    return {
      orderId: order.id,
      authorizationId: auth.authorizationId,
    };
  }
}

輸出適配器

export class PostgresOrderRepository implements OrderRepositoryPort {
  constructor(private readonly db: SqlClient) {}

  async save(order: Order): Promise<void> {
    await this.db.query(
      "insert into orders (id, amount_cents, status, authorization_id) values ($1, $2, $3, $4)",
      [order.id, order.amountCents, order.status, order.authorizationId]
    );
  }

  async findById(orderId: string): Promise<Order | null> {
    const row = await this.db.oneOrNone("select * from orders where id = $1", [orderId]);
    return row ? Order.rehydrate(row) : null;
  }
}

組合根

export const buildCreateOrderUseCase = (deps: { db: SqlClient; stripe: StripeClient }) => {
  const orderRepository = new PostgresOrderRepository(deps.db);
  const paymentGateway = new StripePaymentGateway(deps.stripe);

  return new CreateOrderUseCase(orderRepository, paymentGateway);
};

多語言對應

在不同生態系統中使用相同的邊界規則;僅語法與接線風格有所變化。

  • TypeScript/JavaScript
    • 埠:application/ports/* 作為介面/型別。
    • 用例:類別/函式,使用建構子/引數注入。
    • 適配器:adapters/inbound/*adapters/outbound/*
    • 組合:明確的工廠/容器模組(無隱藏的全域變數)。
  • Java
    • 套件:domainapplication.port.inapplication.port.outapplication.usecaseadapter.inadapter.out
    • 埠:application.port.* 中的介面。
    • 用例:純類別(Spring @Service 為選用,非必要)。
    • 組合:Spring 配置或手動接線類別;將接線邏輯保留在領域/用例類別之外。
  • Kotlin
    • 模組/套件鏡像 Java 的分割(domainapplication.portapplication.usecaseadapter)。
    • 埠:Kotlin 介面。
    • 用例:使用建構子注入的類別(Koin/Dagger/Spring/手動)。
    • 組合:模組定義或專用的組合函式;避免服務定位器模式。
  • Go
    • 套件:internal/<feature>/domainapplicationportsadapters/inboundadapters/outbound
    • 埠:由消費應用套件擁有的小型介面。
    • 用例:具有介面欄位加上明確 New... 建構子的結構體。
    • 組合:在 cmd/<app>/main.go(或專用接線套件)中接線,保持建構子明確。

應避免的反模式

  • 領域實體匯入 ORM 模型、Web 框架型別或 SDK 客戶端。
  • 用例直接從 reqres 或佇列中繼資料讀取。
  • 從用例直接回傳資料庫行,而未經領域/應用對應。
  • 讓適配器直接互相呼叫,而非透過用例埠流動。
  • 將依賴接線分散到許多檔案中,並使用隱藏的全域單例。

遷移手冊

  1. 選擇一個經常變更的垂直切片(單一端點/任務)。
  2. 提取具有明確輸入/輸出型別的用例邊界。
  3. 圍繞現有基礎設施呼叫引入輸出埠。
  4. 將編排邏輯從控制器/服務移至用例中。
  5. 保留舊適配器,但讓它們委派給新用例。
  6. 在新邊界周圍新增測試(單元 + 適配器整合)。
  7. 逐個切片重複;避免全面重寫。

重構現有系統

  • 扼殺者模式:保留當前端點,一次將一個用例路由到新的埠/適配器。
  • 不進行大爆炸式重寫:按功能切片遷移,並透過特徵化測試保留行為。
  • 先建立外觀:在替換內部實作之前,將舊有服務包裝在輸出埠之後。
  • 組合凍結:及早集中接線,使新依賴不會洩漏到領域/用例層。
  • 切片選擇規則:優先處理高變動率、低影響範圍的流程。
  • 回滾路徑:為每個遷移的切片保留可逆的切換或路由開關,直到生產行為得到驗證。

測試指南(相同的六角形邊界)

  • 領域測試:將實體/值物件作為純商業規則進行測試(無模擬、無框架設定)。
  • 用例單元測試:使用輸出埠的假物件/樁件測試編排;斷言商業結果與埠互動。
  • 輸出適配器合約測試:在埠層級定義共享合約套件,並對每個適配器實作執行。
  • 輸入適配器測試:驗證協定對應(HTTP/CLI/佇列負載到用例輸入,以及輸出/錯誤對應回協定)。
  • 適配器整合測試:針對真實基礎設施(資料庫/API/佇列)執行,測試序列化、綱要/查詢行為、重試與逾時。
  • 端到端測試:涵蓋關鍵使用者旅程,從輸入適配器 -> 用例 -> 輸出適配器。
  • 重構安全:在提取前新增特徵化測試;保留它們直到新邊界行為穩定且等效。

最佳實踐檢查清單

  • 領域與用例層僅匯入內部型別與埠。
  • 每個外部依賴都由一個輸出埠表示。
  • 驗證發生在邊界(輸入適配器 + 用例不變條件)。
  • 使用不可變轉換(回傳新值/實體,而非修改共享狀態)。
  • 錯誤在邊界間轉換(基礎設施錯誤 -> 應用/領域錯誤)。
  • 組合根明確且易於審計。
  • 用例可使用簡單的記憶體假埠進行測試。
  • 重構從一個垂直切片開始,並附帶行為保留測試。
  • 語言/框架特定細節保留在適配器中,絕不進入領域規則。