六边形架构模式:端口与适配器、核心领域与外部适配器

六边形架构,也叫端口与适配器模式。我第一次接触它是在一个遗留系统重构项目中。那个系统耦合太严重了,改一个数据库字段,整个业务层都要跟着改。后来我引入了六边形架构,才真正把核心业务从技术细节中解放出来。

说白了,六边形架构的核心思想就一句话:让业务逻辑成为系统的中心,技术实现只是外围的插件。你想想看,业务逻辑才是你系统的灵魂,数据库、消息队列、Web框架这些,都是可以替换的。

架构的核心三要素

六边形架构由三个核心部分组成。我习惯把它们想象成一个洋葱:

  • 核心领域(Core Domain):最内层,纯业务逻辑,不依赖任何外部框架
  • 端口(Ports):定义业务接口,是核心领域与外界沟通的契约
  • 适配器(Adapters):实现端口,连接外部世界(数据库、Web、消息队列等)

这里有个关键点:依赖方向永远是从外向内。适配器依赖端口,端口依赖核心领域。核心领域对适配器一无所知。

核心原则:业务代码中不要出现任何import语句指向外部框架。如果你在业务类里看到了@Autowired、@Entity、@RequestMapping,那说明架构已经跑偏了。

SVG架构图:六边形架构的核心逻辑

核心领域 实体 / 值对象 领域服务 端口层(Ports) 入站端口 (接口) 出站端口 (接口) 适配器层(Adapters) REST控制器 (入站适配器) 消息消费者 (入站适配器) JPA仓库 (出站适配器) 消息生产者 (出站适配器) 核心领域 端口 适配器

端口:核心领域与外界沟通的契约

端口就是接口。我把它分为两类:

  • 入站端口(Inbound Port):外部如何调用我的业务?比如用户下单、查询订单
  • 出站端口(Outbound Port):我的业务需要外部提供什么?比如保存订单、发送通知

来看一个订单系统的例子。这是入站端口:

// 入站端口:定义业务能力
public interface OrderService {
    Order createOrder(CreateOrderCommand command);
    Order findOrderById(String orderId);
    void cancelOrder(String orderId);
}

这是出站端口:

// 出站端口:定义持久化契约
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(String orderId);
    void deleteById(String orderId);
}

注意看,这两个接口里没有任何Spring注解,没有JPA依赖。它们就是纯粹的Java接口。这就是核心领域该有的样子。

核心领域:纯业务逻辑

核心领域里只有实体、值对象和领域服务。它们不关心数据怎么存,也不关心请求从哪里来。

// 核心领域实体
public class Order {
    private String orderId;
    private OrderStatus status;
    private List<OrderItem> items;
    private Money totalAmount;

    public Order(String orderId, List<OrderItem> items) {
        this.orderId = orderId;
        this.items = items;
        this.status = OrderStatus.CREATED;
        this.totalAmount = calculateTotal();
    }

    public void cancel() {
        if (this.status == OrderStatus.SHIPPED) {
            throw new IllegalStateException("已发货订单不能取消");
        }
        this.status = OrderStatus.CANCELLED;
    }

    private Money calculateTotal() {
        return items.stream()
                .map(OrderItem::getSubtotal)
                .reduce(Money.ZERO, Money::add);
    }
}

这个Order类里,没有@Entity,没有@Table,没有@Column。它就是一个纯粹的领域对象。我在项目中见过太多把@Entity注解直接写在领域模型上的做法,结果业务逻辑和ORM绑定死了,想换数据库都难。

我的习惯:核心领域的包名就叫domain,里面只放纯Java类。所有框架注解都放在适配器层。这样即使将来从Spring Boot换成Quarkus,核心代码一行都不用改。

适配器:连接外部世界的桥梁

适配器分两种:入站适配器和出站适配器。

入站适配器接收外部请求,调用入站端口。比如REST控制器:

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final OrderService orderService;  // 入站端口

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
        CreateOrderCommand command = new CreateOrderCommand(request.getItems());
        Order order = orderService.createOrder(command);
        return ResponseEntity.ok(OrderResponse.from(order));
    }
}

出站适配器实现出站端口,与外部系统交互。比如JPA仓库:

@Repository
public class JpaOrderRepository implements OrderRepository {
    private final SpringDataJpaOrderRepository jpaRepository;

    public JpaOrderRepository(SpringDataJpaOrderRepository jpaRepository) {
        this.jpaRepository = jpaRepository;
    }

    @Override
    public void save(Order order) {
        OrderEntity entity = OrderEntity.from(order);
        jpaRepository.save(entity);
    }

    @Override
    public Optional<Order> findById(String orderId) {
        return jpaRepository.findById(orderId)
                .map(OrderEntity::toDomain);
    }
}

看到没有?OrderRepository接口在核心领域里定义,JpaOrderRepository在适配器层实现。核心领域根本不知道JPA的存在。

我曾经踩过的坑:在核心领域里直接使用@Transactional注解。后来想换成R2DBC响应式数据库,发现事务注解散落在各个业务方法里,改起来痛不欲生。正确的做法是:事务控制放在适配器层,核心领域只关心业务规则。

Spring Boot实践:完整项目结构

我建议的项目包结构是这样的:

com.example.ordersystem
├── domain                    # 核心领域
│   ├── model                 # 实体、值对象
│   │   ├── Order.java
│   │   ├── OrderItem.java
│   │   └── OrderStatus.java
│   ├── port                  # 端口定义
│   │   ├── inbound
│   │   │   └── OrderService.java
│   │   └── outbound
│   │       └── OrderRepository.java
│   └── service               # 领域服务
│       └── OrderDomainService.java
├── adapter                   # 适配器层
│   ├── inbound               # 入站适配器
│   │   ├── rest
│   │   │   ├── OrderController.java
│   │   │   └── dto
│   │   │       ├── CreateOrderRequest.java
│   │   │       └── OrderResponse.java
│   │   └── messaging
│   │       └── OrderMessageConsumer.java
│   └── outbound              # 出站适配器
│       ├── persistence
│       │   ├── JpaOrderRepository.java
│       │   └── entity
│       │       └── OrderEntity.java
│       └── messaging
│           └── OrderMessageProducer.java
└── Application.java          # Spring Boot启动类

这个结构的好处是:依赖关系一目了然。domain层不依赖adapter层,adapter层依赖domain层。你可以在IDE里看到,domain包下的import语句全是Java标准库,没有Spring、没有JPA。

六边形架构的优势

维度 传统分层架构 六边形架构
依赖方向 上层依赖下层,层层耦合 外部依赖内部,核心独立
测试难度 需要启动Spring容器 核心逻辑纯单元测试,无需框架
技术替换 改数据库要改业务层 只替换适配器,核心不变
代码复用 业务逻辑与框架绑定 核心逻辑可跨框架复用

我记得有一次,客户要求把MySQL换成MongoDB。在传统架构下,这几乎要重写整个持久层。但在六边形架构下,我只需要新增一个MongoOrderRepository实现OrderRepository接口,然后改一下配置类注入。核心领域代码一行没动。嗯,这就是架构设计带来的价值。

总结一下:六边形架构不是银弹,但它能帮你把业务逻辑和技术实现解耦。如果你正在做一个业务复杂度高、技术栈可能变化的系统,我强烈建议你试试。从一个小模块开始,慢慢你就会发现它的好处。

公众号:蓝海资料掘金营,微信deep3321