六边形架构(端口与适配器模式)
六边形架构(Ports and Adapters)让业务逻辑独立于框架、传输与持久化细节。核心应用只依赖抽象「端口」,「适配器」在边缘实现这些端口。依赖方向永远向内:适配器 → 应用/领域;应用 → 端口契约(入站/出站);领域 → 仅领域内抽象,绝不依赖框架或基础设施。
何时使用
适用:
- 为新特性建立边界,长期可维护性与可测试性是首要诉求
- 重构分层式或框架重的代码,领域逻辑与 I/O 混在一起需要剥离
- 同一用例要支持多种入口(HTTP、CLI、队列消费者、定时任务)
- 替换基础设施(数据库、外部 API、消息总线)而不重写业务规则
不该用(负边界):
- 简单 CRUD —— 套端口适配器属于过度设计
- 纯前端,不涉及后端领域核心
- 只是局部小范围重构
步骤
- 建模一个用例边界:定义单个用例及清晰的输入/输出 DTO;把传输细节(Express
req、GraphQLcontext、任务负载包装)挡在边界之外。 - 先定义出站端口:把每个副作用建模成端口——持久化(
UserRepositoryPort)、外部调用(BillingGatewayPort)、横切(LoggerPort、ClockPort)。端口建模「能力」,不绑定「技术」。 - 用纯编排实现用例:用例类/函数经构造器或参数注入端口;它校验应用级不变量、协调领域规则、返回普通数据结构。
- 在边缘构建适配器:入站适配器把协议输入转成用例输入;出站适配器把应用契约映射到具体 API/ORM/查询构造器;映射只留在适配器里,不进用例。
- 在组合根装配一切:实例化适配器,再注入用例。装配集中在一处,避免隐式的服务定位器(service locator)行为。
- 按边界测试:用例用假端口做单元测试;适配器对真实基础设施做集成测试;面向用户的流程经入站适配器做端到端测试。
指令
核心概念:
- 领域模型:业务规则与实体/值对象,不导入任何框架。
- 用例(应用层):编排领域行为与工作流步骤。
- 入站端口:描述应用「能做什么」的契约(命令/查询/用例接口)。
- 出站端口:描述应用「需要什么」的契约(仓储、网关、事件发布、时钟、UUID 等)。出站端口接口通常住在应用层(仅当抽象确属领域级时才放领域层)。
- 适配器:端口的基础设施与交付实现(HTTP 控制器、DB 仓储、队列消费者、SDK 包装)。
- 组合根:把具体适配器绑定到用例的唯一装配位置。
推荐目录(特性优先 + 显式边界):
src/features/orders/
domain/ Order.ts OrderPolicy.ts
application/
ports/inbound/ CreateOrder.ts
ports/outbound/ OrderRepositoryPort.ts PaymentGatewayPort.ts
use-cases/ CreateOrderUseCase.ts
adapters/inbound/http/ createOrderRoute.ts
adapters/outbound/postgres/ PostgresOrderRepository.ts
adapters/outbound/stripe/ StripePaymentGateway.ts
composition/ ordersContainer.ts
跨语言映射(边界规则不变,仅语法与装配方式变):
- TS/JS:端口为
application/ports/*接口;用例为构造器注入的类/函数;组合用显式工厂/容器模块(无隐藏全局)。 - Java:包
domain/application.port.in/application.port.out/application.usecase/adapter.in/adapter.out;用例为普通类(Spring@Service可选非必须);装配放 Spring 配置或手写装配类,不进领域/用例。 - Kotlin:包结构对齐 Java;端口为 interface;用例构造器注入(Koin/Dagger/Spring/手动);用模块定义或专用组合函数,避免服务定位器。
- Go:包
internal/<feature>/domain/application/ports/adapters/{inbound,outbound};端口为消费方拥有的小接口;用例为带接口字段的 struct + 显式New...构造器;装配在cmd/<app>/main.go。
示例
出站端口(仅契约,无实现):
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 }>;
}
用例(经构造器注入端口;纯编排,返回普通数据):
export class CreateOrderUseCase {
constructor(
private readonly orderRepository: OrderRepositoryPort,
private readonly paymentGateway: PaymentGatewayPort
) {}
async execute(input: { orderId: string; amountCents: number }) {
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 }) =>
new CreateOrderUseCase(
new PostgresOrderRepository(deps.db),
new StripePaymentGateway(deps.stripe)
);
迁移到六边形(绞杀者式,按切片推进,不大爆炸重写):
- 选一条高频变更痛点的纵切(单个端点/任务)。
- 抽出带显式输入/输出类型的用例边界。
- 在既有基础设施调用周围引入出站端口(外观先行:先用端口包住遗留服务,再换内部实现)。
- 把编排逻辑从控制器/服务搬进用例。
- 保留旧适配器,但让其委派到新用例。
- 围绕新边界加测试(单元 + 适配器集成)。
- 逐切片重复;每个切片保留可逆开关/路由切换,验证生产行为前可回滚。
注意事项
规避反模式:
- 领域实体导入 ORM 模型、Web 框架类型或 SDK 客户端。
- 用例直接读
req/res或队列元数据。 - 用例不经领域/应用映射,直接返回数据库行。
- 适配器之间直接互调,而非经用例端口流转。
- 装配散落多文件 + 隐藏全局单例。
测试遵循同一套边界:领域测试当作纯业务规则(无 mock、无框架);用例单元测试用假端口断言业务结果与端口交互;出站适配器写共享契约测试套件,跑在每个实现上;入站适配器测协议映射;适配器集成测试跑真实基础设施(序列化、schema/查询、重试、超时)。重构前先加特征化测试(characterization test),保留到新边界行为稳定等价。
通用约束:本技能输出不替代针对具体环境的验证、测试与专家评审;若领域边界、安全边界或成功标准等关键输入缺失,应先停下澄清。
互见
- related:
backend-architecture-patterns(整洁/六边形/DDD 全景)、clean-code-principles、ddd-strategic-design、ddd-context-mapping、event-sourcing-cqrs、api-design-principles - combines_with:
zero-downtime-migration-architect/database-migration-strategies(绞杀者迁移时的零停机与数据迁移)、api-test-suite-builder(按边界落地契约与集成测试)
采编自 affaan-m/everything-claude-code(MIT)。