重构与架构演进:从分层架构到六边形架构的重构路径

说实话,分层架构是我们最熟悉的架构模式了。Controller、Service、DAO三层一搭,项目就能跑起来。但我在实际项目中遇到过一个问题——当业务逻辑越来越复杂,分层架构开始显得力不从心。数据库换了、消息队列换了、外部接口换了,每次改动都要动到核心业务代码。这时候,六边形架构就派上用场了。

今天我们就聊聊,怎么从分层架构平滑地演进到六边形架构。这不是一次性的推倒重来,而是一条渐进的重构路径。

分层架构的痛点

先说说分层架构的问题在哪。你想想看,一个典型的Spring Boot项目,Controller层直接调用Service,Service层直接操作Repository。看起来挺清晰,对吧?

但问题来了:

  • 依赖方向是单向的——上层依赖下层,但下层的变化会波及上层
  • 业务逻辑和基础设施耦合——Service里到处都是@Autowired的Repository、RestTemplate、KafkaTemplate
  • 测试困难——想测个业务逻辑,得先把数据库、消息队列全搭起来

我在一个金融项目中就吃过这个亏。当时要换数据库从MySQL到PostgreSQL,本以为只是改个配置的事,结果发现业务代码里到处都是SQL方言的写法,改了一个星期才搞定。说白了,就是分层架构没把业务和基础设施真正隔离开。

六边形架构的核心思想

六边形架构,也叫端口适配器架构。它的核心就一句话:业务逻辑在中心,外部世界在四周

我习惯把它理解成洋葱。最里面是领域模型和业务逻辑,外面一层是端口(Port),最外面是适配器(Adapter)。端口定义了业务需要什么能力,适配器去实现这些能力。

关键区别:

  • 分层架构:业务层依赖数据层(依赖方向朝内)
  • 六边形架构:数据层依赖业务层(依赖方向朝外)

说白了,依赖关系反转了。业务层不再关心数据存在哪、消息发到哪,它只定义接口。具体实现由外部的适配器来完成。

SVG:从分层到六边形的演进路径

分层架构 Controller 层 接收请求,参数校验 Service 层 业务逻辑,事务管理 DAO / Repository 层 数据访问,SQL操作 数据库 / 外部系统 MySQL, Redis, MQ 演进 六边形架构 领域模型 核心业务逻辑 端口定义 Web适配器 RPC适配器 消息适配器 DB适配器 缓存适配器 MQ适配器 依赖方向:适配器 → 端口 → 领域 业务层不依赖任何外部框架

重构路径:三步走

从分层架构到六边形架构,我建议分三步走。步子太大容易扯着蛋,咱们一步步来。

第一步:识别核心领域

先别急着改代码。拿出你的Service层,看看哪些是真正的业务逻辑,哪些只是数据搬运。我习惯用这个标准判断:如果换数据库,这段代码要不要改?如果要改,说明它和基础设施耦合了。

举个例子,一个订单Service:

// 重构前:分层架构的典型写法
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private PaymentClient paymentClient;

    public Order createOrder(OrderRequest request) {
        // 业务逻辑
        Order order = new Order();
        order.setAmount(request.getAmount());
        order.setStatus("PENDING");

        // 基础设施调用混在一起
        orderRepository.save(order);
        paymentClient.charge(request.getAmount());

        return order;
    }
}

这里的问题很明显:业务逻辑和数据库操作、外部支付调用全混在一起。测试的时候,你得Mock两个东西。

第二步:提取端口接口

把业务逻辑依赖的外部能力抽象成接口。这些接口就是端口(Port)。

// 定义端口:业务层只依赖这个接口
public interface OrderRepository {
    void save(Order order);
}

public interface PaymentService {
    void charge(BigDecimal amount);
}

// 重构后的业务逻辑:纯领域
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;

    public OrderService(OrderRepository repo, PaymentService pay) {
        this.orderRepository = repo;
        this.paymentService = pay;
    }

    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setAmount(request.getAmount());
        order.setStatus("PENDING");

        orderRepository.save(order);
        paymentService.charge(request.getAmount());

        return order;
    }
}

嗯,看起来变化不大?别急,关键在下一步。

第三步:实现适配器

端口定义好了,适配器去实现它们。适配器放在外层,可以随时替换。

// 适配器:实现端口,依赖具体技术
@Repository
public class JpaOrderRepository implements OrderRepository {
    private final JpaRepository jpaRepository;

    @Override
    public void save(Order order) {
        // JPA的具体实现
        jpaRepository.save(order);
    }
}

// 另一个适配器:如果换MyBatis,再写一个
public class MyBatisOrderRepository implements OrderRepository {
    private final SqlSession sqlSession;

    @Override
    public void save(Order order) {
        sqlSession.insert("insertOrder", order);
    }
}

我的习惯:端口接口放在领域层(domain包),适配器放在基础设施层(infrastructure包)。这样领域层没有任何框架注解,纯POJO,测试起来特别爽。

避坑指南

我曾经踩过的坑:

  • 过度抽象——一开始就把所有东西都抽象成端口,结果接口比实现还多。建议只抽象那些可能变化的外部依赖。
  • 端口粒度太细——每个方法一个接口,导致适配器类爆炸。我习惯按业务聚合来定义端口,比如OrderRepository包含所有订单持久化操作。
  • 忽略事务边界——六边形架构下,事务通常由适配器管理。别在领域层开事务,那是基础设施的事。

演进过程中的常见问题

问题 原因 解决方案
领域层依赖Spring注解 习惯性在领域类上加@Service、@Transactional 把注解移到适配器层,领域层保持纯Java
端口定义过多 每个外部调用都抽象成端口 按业务聚合,一个端口对应一个外部服务
测试仍然困难 适配器层没做好Mock 端口接口天然可Mock,测试时注入Mock适配器
重构范围太大 想一次性改完所有模块 按模块逐步演进,先改核心业务模块

什么时候该用六边形架构?

不是所有项目都需要六边形架构。我个人觉得,以下情况值得考虑:

  • 业务逻辑复杂——领域模型有大量规则和计算
  • 外部依赖多变——可能换数据库、换消息队列、对接多个第三方
  • 需要高质量测试——业务逻辑需要单元测试覆盖,不想依赖外部环境
  • 团队有DDD经验——六边形架构和领域驱动设计是绝配

如果只是一个简单的CRUD应用,分层架构完全够用。别为了架构而架构,那是过度设计。

写在最后

从分层架构到六边形架构,本质上是一次依赖关系的反转。把业务逻辑从基础设施的束缚中解放出来,让核心代码更纯粹、更可测、更可维护。

重构的节奏很重要。我建议先从最核心、最易变的模块开始,比如订单、支付这类业务。改完一个模块,跑通测试,再改下一个。别想着一步到位,渐进式演进才是王道。

记住一句话:架构是演进而来的,不是设计出来的。


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