六边形架构:端口与适配器模式

FreeGuideOnline 最新 2026-06-18

六边形架构(端口与适配器模式)完全指南

什么是六边形架构?

六边形架构,又称端口与适配器模式(Ports and Adapters Architecture),是由Alistair Cockburn在2005年提出的一种软件架构风格。它的核心目标是将业务逻辑从所有外部依赖中彻底解耦,使得应用程序的核心可以独立于技术细节进行开发、测试和维护。

想象一个六边形,中心是我们的应用程序核心(领域逻辑),每个边代表一个端口,通过端口与外界通信。外部系统(数据库、UI、消息队列等)通过适配器连接到这些端口。这种对称性使得系统内外边界清晰,可互换性极强。

为什么需要六边形架构?

传统分层架构(如三层架构)经常导致业务逻辑泄漏到基础设施层,或者基础设施代码侵入领域层。随着时间推移,系统变得僵化,难以替换技术组件(如从REST切换到gRPC,或从MySQL迁移到MongoDB)。六边形架构通过明确的边界和依赖倒置解决了这些问题,带来以下核心价值:

  • 业务逻辑纯粹:核心代码不依赖任何框架或外部库。
  • 可测试性极强:核心业务可以在没有数据库、网络的情况下快速单元测试。
  • 技术替换成本低:更换数据库或用户界面只需添加新的适配器,核心不变。
  • 可独立部署与开发:团队成员可以并行工作在核心和适配器上。

核心概念:端口与适配器

端口(Ports)

端口是核心应用程序定义的接口,描述了核心需要什么(驱动侧主端口)或提供什么(被驱动侧辅端口)。它们处于六边形的边界上,是业务语言与外界之间的契约。

  • 驱动端口(Driving Ports):由核心提供、供外部调用的接口,通常是应用服务接口(例如PlaceOrderUseCase)。外部参与者(如HTTP控制器)通过调用这些端口来驱动应用程序行为。
  • 被驱动端口(Driven Ports):核心需要调用外部服务的抽象接口(例如OrderRepositoryPaymentGateway)。它们定义了核心依赖的外部能力,但核心不关心实现细节。

端口本质上是普通编程语言中的接口(Interface)或抽象类,不包含任何技术实现代码。

适配器(Adapters)

适配器是端口的具体实现,它们将外部特定的技术转换成端口所定义的通用合约。适配器位于六边形之外,分为两类:

  • 驱动适配器(Driving Adapters):接收外部输入并将其转换为对驱动端口的调用。例如,一个Spring MVC Controller实现了HTTP请求处理,调用PlaceOrderUseCase。另一例是消息监听器或CLI命令处理器。
  • 被驱动适配器(Driven Adapters):实现被驱动端口接口,将核心的业务调用转换为具体的存储、网络操作。例如,一个JPA持久化适配器实现了OrderRepository,或者一个Stripe支付适配器实现了PaymentGateway

关键点:依赖方向始终指向内部。适配器依赖端口(由核心定义),核心从不依赖适配器。


六边形架构的包结构示例

在项目中,可以通过清晰的包结构强制架构边界:

com.example.order
├── domain                  // 领域实体、值对象
├── application
│   ├── port
│   │   ├── driving        // 驱动端口接口
│   │   └── driven         // 被驱动端口接口
│   └── service            // 应用服务(驱动端口实现)
├── adapter
│   ├── web                // 驱动适配器(REST控制器等)
│   ├── persistence        // 被驱动适配器(JPA仓库实现等)
│   └── messaging          // 其他适配器
└── config                 // 依赖注入配置

注意:领域层与应用层共同构成六边形的内部核心,它们对任何适配器一无所知。


动手实现:一个订单服务示例

我们将用伪代码展示如何运用六边形架构构建一个简单的下单流程。

1. 定义被驱动端口(核心需要的接口)

// 被驱动端口:订单仓储
public interface OrderRepository {
    void save(Order order);
    Order findById(OrderId id);
}

// 被驱动端口:支付网关
public interface PaymentService {
    PaymentResult process(PaymentDetails details);
}

2. 定义驱动端口(核心提供的用例)

public interface PlaceOrderUseCase {
    OrderId placeOrder(PlaceOrderCommand command);
}

3. 实现驱动端口(应用服务)

public class PlaceOrderService implements PlaceOrderUseCase {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;

    // 依赖注入端口接口,而非具体实现
    public PlaceOrderService(OrderRepository orderRepository,
                             PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
    }

    @Override
    public OrderId placeOrder(PlaceOrderCommand command) {
        // 领域逻辑(此处简略)
        Order order = Order.create(command.getItems());
        
        // 调用支付端口(被驱动)
        PaymentResult result = paymentService.process(command.getPayment());
        if (result.isSuccess()) {
            order.markAsPaid();
            orderRepository.save(order);
            return order.getId();
        }
        throw new PaymentFailedException();
    }
}

核心完全不知道支付是用Stripe还是PayPal,也不知道存储是PostgreSQL还是内存。

4. 创建驱动适配器(REST API)

@RestController
@RequestMapping("/orders")
public class OrderController {
    private final PlaceOrderUseCase placeOrderUseCase;

    // 注入驱动端口
    public OrderController(PlaceOrderUseCase placeOrderUseCase) {
        this.placeOrderUseCase = placeOrderUseCase;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
        PlaceOrderCommand command = toCommand(request);
        OrderId id = placeOrderUseCase.placeOrder(command);
        return ResponseEntity.status(201).body(new OrderResponse(id));
    }
}

5. 创建被驱动适配器(持久化)

@Repository
public class JpaOrderRepository implements OrderRepository {
    private final JpaOrderSpringRepository springRepo;

    @Override
    public void save(Order order) {
        springRepo.save(toJpaEntity(order));
    }

    @Override
    public Order findById(OrderId id) {
        // ... 调用Spring Data
    }
}

6. 组装应用(依赖注入配置)

在这种架构中,配置层负责将适配器组装进端口,典型使用依赖注入容器完成。


六边形架构与其他架构的对比

特性 六边形架构 传统分层架构 干净架构(Clean Architecture)
核心依赖方向 依赖倒置,全部指向核心 从上到下(通常UI->业务->数据) 同六边形,更强调用例/实体
端口抽象 明确的端口和适配器概念 通常通过接口(DAO)但不够严格 通过边界接口(Input/Output边界)
外部替换难度 极易,添加新适配器即可 较难,通常业务与基础设施耦合 易,与六边形理念同根
学习曲线 中等,概念较少 中高,概念较多(实体、用例、接口适配器)

六边形架构可视为干净架构的一种具体实现变体,它用“六边形”形象化了“端口-适配器”的隔离思想,在DDD(领域驱动设计)社区中被广泛采纳。


如何避免常见误区

1. 过度抽象

不是每个组件都需要端口。对于稳定且很少更换的技术(如日志库),不必为其创建抽象端口。六边形架构的目标是隔离业务意图技术实现,而不是无差别抽象所有依赖。

2. 包结构和代码组织混乱

使用模块化结构,确保架构规则在代码组织层面可见。考虑使用ArchUnit等工具在测试中强制检查依赖规则,避免适配器之间的直接引用。

3. 应用服务过重

将业务逻辑放在领域实体和值对象中,应用服务只负责编排和协调端口调用。避免在应用服务中包含大量的条件判断和计算,那是领域层的职责。

4. 忽略异步和事件驱动适配器

端口不一定是同步接口。可以为事件发布/订阅定义被驱动端口,将消息队列适配器作为驱动适配器或混合适配器实现。六边形架构天然支持事件驱动。


测试策略

六边形架构带来的最大红利之一就是极简的测试设置:

  • 核心单元测试:直接实例化领域实体和应用服务,使用mock或stub的端口接口。无需Spring容器、数据库或HTTP服务器。
  • 适配器集成测试:单独测试每个适配器(如数据库适配器用真实测试数据库,REST API用MockMvc)。核心使用真正的端口实现但测试范围限定在适配器边界。
  • 全栈烟雾测试:整体组装所有适配器,用端到端测试验证关键流程。

选择六边形架构的时机

六边形架构非常适合以下场景:

  • 业务逻辑复杂且预期长期维护:需要保护核心资产免受技术变化侵蚀。
  • 技术栈频繁变动或多样化:需要支持多种客户端(Web、移动端、第三方API)或需要切换基础设施提供商。
  • 大型团队协作:清晰的边界使得不同团队可独立开发和测试各自适配器。
  • 微服务或模块化单体:每个服务/模块内部采用六边形架构能保证高内聚低耦合。

对于简单的CRUD应用或一次性原型,六边形架构可能显得负担过重。应根据项目规模与寿命灵活判断。


总结

六边形架构通过“端口和适配器”这一简单而强大的隐喻,彻底将业务意图从实现细节中解放出来。它不仅是技术实践,更是一种设计思想:让外部依赖成为核心的插件,而核心永远保持稳定和纯粹。从今天起,在你的项目中尝试引入哪怕一个端口-适配器对,体验核心代码在极速单元测试中运行的自由感,你会切身体会到这种架构的魅力。