重构与架构演进:从分层架构到六边形架构的重构路径
说实话,分层架构是我们最熟悉的架构模式了。Controller、Service、DAO三层一搭,项目就能跑起来。但我在实际项目中遇到过一个问题——当业务逻辑越来越复杂,分层架构开始显得力不从心。数据库换了、消息队列换了、外部接口换了,每次改动都要动到核心业务代码。这时候,六边形架构就派上用场了。
今天我们就聊聊,怎么从分层架构平滑地演进到六边形架构。这不是一次性的推倒重来,而是一条渐进的重构路径。
分层架构的痛点
先说说分层架构的问题在哪。你想想看,一个典型的Spring Boot项目,Controller层直接调用Service,Service层直接操作Repository。看起来挺清晰,对吧?
但问题来了:
- 依赖方向是单向的——上层依赖下层,但下层的变化会波及上层
- 业务逻辑和基础设施耦合——Service里到处都是@Autowired的Repository、RestTemplate、KafkaTemplate
- 测试困难——想测个业务逻辑,得先把数据库、消息队列全搭起来
我在一个金融项目中就吃过这个亏。当时要换数据库从MySQL到PostgreSQL,本以为只是改个配置的事,结果发现业务代码里到处都是SQL方言的写法,改了一个星期才搞定。说白了,就是分层架构没把业务和基础设施真正隔离开。
六边形架构的核心思想
六边形架构,也叫端口适配器架构。它的核心就一句话:业务逻辑在中心,外部世界在四周。
我习惯把它理解成洋葱。最里面是领域模型和业务逻辑,外面一层是端口(Port),最外面是适配器(Adapter)。端口定义了业务需要什么能力,适配器去实现这些能力。
关键区别:
- 分层架构:业务层依赖数据层(依赖方向朝内)
- 六边形架构:数据层依赖业务层(依赖方向朝外)
说白了,依赖关系反转了。业务层不再关心数据存在哪、消息发到哪,它只定义接口。具体实现由外部的适配器来完成。
SVG:从分层到六边形的演进路径
重构路径:三步走
从分层架构到六边形架构,我建议分三步走。步子太大容易扯着蛋,咱们一步步来。
第一步:识别核心领域
先别急着改代码。拿出你的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