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) - 橫切關注點(
LoggerPort、ClockPort)
埠應建模能力,而非技術。
步驟 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
- 套件:
domain、application.port.in、application.port.out、application.usecase、adapter.in、adapter.out。 - 埠:
application.port.*中的介面。 - 用例:純類別(Spring
@Service為選用,非必要)。 - 組合:Spring 配置或手動接線類別;將接線邏輯保留在領域/用例類別之外。
- 套件:
- Kotlin
- 模組/套件鏡像 Java 的分割(
domain、application.port、application.usecase、adapter)。 - 埠:Kotlin 介面。
- 用例:使用建構子注入的類別(Koin/Dagger/Spring/手動)。
- 組合:模組定義或專用的組合函式;避免服務定位器模式。
- 模組/套件鏡像 Java 的分割(
- Go
- 套件:
internal/<feature>/domain、application、ports、adapters/inbound、adapters/outbound。 - 埠:由消費應用套件擁有的小型介面。
- 用例:具有介面欄位加上明確
New...建構子的結構體。 - 組合:在
cmd/<app>/main.go(或專用接線套件)中接線,保持建構子明確。
- 套件:
應避免的反模式
- 領域實體匯入 ORM 模型、Web 框架型別或 SDK 客戶端。
- 用例直接從
req、res或佇列中繼資料讀取。 - 從用例直接回傳資料庫行,而未經領域/應用對應。
- 讓適配器直接互相呼叫,而非透過用例埠流動。
- 將依賴接線分散到許多檔案中,並使用隱藏的全域單例。
遷移手冊
- 選擇一個經常變更的垂直切片(單一端點/任務)。
- 提取具有明確輸入/輸出型別的用例邊界。
- 圍繞現有基礎設施呼叫引入輸出埠。
- 將編排邏輯從控制器/服務移至用例中。
- 保留舊適配器,但讓它們委派給新用例。
- 在新邊界周圍新增測試(單元 + 適配器整合)。
- 逐個切片重複;避免全面重寫。
重構現有系統
- 扼殺者模式:保留當前端點,一次將一個用例路由到新的埠/適配器。
- 不進行大爆炸式重寫:按功能切片遷移,並透過特徵化測試保留行為。
- 先建立外觀:在替換內部實作之前,將舊有服務包裝在輸出埠之後。
- 組合凍結:及早集中接線,使新依賴不會洩漏到領域/用例層。
- 切片選擇規則:優先處理高變動率、低影響範圍的流程。
- 回滾路徑:為每個遷移的切片保留可逆的切換或路由開關,直到生產行為得到驗證。
測試指南(相同的六角形邊界)
- 領域測試:將實體/值物件作為純商業規則進行測試(無模擬、無框架設定)。
- 用例單元測試:使用輸出埠的假物件/樁件測試編排;斷言商業結果與埠互動。
- 輸出適配器合約測試:在埠層級定義共享合約套件,並對每個適配器實作執行。
- 輸入適配器測試:驗證協定對應(HTTP/CLI/佇列負載到用例輸入,以及輸出/錯誤對應回協定)。
- 適配器整合測試:針對真實基礎設施(資料庫/API/佇列)執行,測試序列化、綱要/查詢行為、重試與逾時。
- 端到端測試:涵蓋關鍵使用者旅程,從輸入適配器 -> 用例 -> 輸出適配器。
- 重構安全:在提取前新增特徵化測試;保留它們直到新邊界行為穩定且等效。
最佳實踐檢查清單
- 領域與用例層僅匯入內部型別與埠。
- 每個外部依賴都由一個輸出埠表示。
- 驗證發生在邊界(輸入適配器 + 用例不變條件)。
- 使用不可變轉換(回傳新值/實體,而非修改共享狀態)。
- 錯誤在邊界間轉換(基礎設施錯誤 -> 應用/領域錯誤)。
- 組合根明確且易於審計。
- 用例可使用簡單的記憶體假埠進行測試。
- 重構從一個垂直切片開始,並附帶行為保留測試。
- 語言/框架特定細節保留在適配器中,絕不進入領域規則。






