重构与微服务:重构单体应用中的模块边界
说实话,微服务这个话题已经被聊烂了。但真正动手拆过单体应用的人,心里都清楚——拆服务不是技术问题,是边界问题。
我记得几年前接手过一个电商后台,代码堆了六七年,一个订单模块里混着库存扣减、优惠券核销、甚至还有用户积分。你想想看,这哪是模块?这分明是意大利面。当时团队想上微服务,结果一讨论,光「订单服务该不该管库存」就吵了两周。
所以这一章,我不跟你扯什么DDD理论。咱们就聊聊,怎么在实际代码里,把那些纠缠不清的模块边界,一刀一刀理清楚。
模块边界模糊的典型症状
先看看你的项目有没有这些「病」:
- 跨模块直接调用内部方法——比如支付模块直接调了订单模块的私有DAO方法。
- 共享数据库表——两个模块读写同一张表,改字段要两边一起改。
- 循环依赖——A模块引了B模块,B模块又引了A模块。
- 一个类干三件事——比如OrderService里同时处理订单、发短信、更新库存。
我在项目中遇到过最夸张的一个案例:一个UserService类,两千多行,里面混着登录、权限、消息推送、甚至还有文件上传。说白了,这就是「上帝类」——什么都能干,什么都耦合。
重构第一步:识别真正的业务边界
怎么找边界?我个人的习惯是:看变化频率。
举个例子,订单的「创建」和「取消」经常变,但「支付回调」相对稳定。如果它们混在一个模块里,每次改订单逻辑都要重新测试支付回调,这就是边界没划好。
具体做法:
- 把每个模块的「变更日志」拉出来,看看哪些功能经常一起改。
- 把经常一起改的功能圈成一个「候选模块」。
- 检查这个候选模块对外暴露的接口——如果接口数量超过5个,说明边界可能太粗了。
重构第二步:用接口隔离代替直接依赖
边界划好了,接下来就是代码层面的解耦。核心原则就一条:模块之间只通过接口通信,不共享内部实现。
看个反面例子:
// 错误做法:支付模块直接调用订单模块的内部方法
public class PaymentService {
public void pay(String orderId) {
// 直接调用订单模块的DAO
OrderDAO dao = new OrderDAO();
Order order = dao.findById(orderId);
// ... 处理支付
}
}
这种写法,一旦OrderDAO改了表结构,支付模块也得跟着改。说白了,耦合死了。
正确的做法:
// 订单模块对外暴露接口
public interface OrderService {
Order getOrder(String orderId);
void updateOrderStatus(String orderId, OrderStatus status);
}
// 支付模块只依赖接口
public class PaymentService {
private OrderService orderService; // 通过依赖注入
public void pay(String orderId) {
Order order = orderService.getOrder(orderId);
// ... 处理支付
orderService.updateOrderStatus(orderId, OrderStatus.PAID);
}
}
嗯,这里要注意:接口的定义权应该属于「被调用方」,也就是订单模块。支付模块只管用,不要自己去定义订单接口。
重构第三步:数据边界——每个模块拥有自己的数据
这是最难的一步。很多团队拆了服务,但数据库还是共用的。结果呢?一个SQL查询跨了三个模块的表,性能问题一出,谁都不敢动。
我建议的做法是:先逻辑隔离,再物理隔离。
逻辑隔离:每个模块只能通过API访问其他模块的数据,不能直接查表。
物理隔离:等逻辑隔离稳定了,再把数据库拆开。比如订单模块的订单表,和支付模块的支付流水表,分到不同的数据库实例。
核心逻辑:模块边界重构的决策树
下面这张图,是我在实际项目中总结的决策流程。每次遇到边界模糊的模块,我就拿它来走一遍:
这张图的逻辑其实很简单:先看变更频率,再看依赖关系,最后处理数据耦合。每一步都走通了,边界自然就清晰了。
避坑指南
最后分享几个我踩过的坑:
- 不要为了微服务而微服务。 我曾经把一个只有三个方法的工具类拆成独立服务,结果引入网络延迟,性能反而下降了。模块边界重构的目标是「高内聚低耦合」,不是「拆得越碎越好」。
- 接口设计要预留扩展点。 比如订单状态变更,不要只返回成功/失败,最好返回一个事件对象,让调用方自己决定怎么处理。这样以后加新逻辑,不用改接口。
- 数据拆分要渐进。 别想着一天拆完。我习惯先做「读写分离」——读操作走缓存或视图,写操作走主库。等稳定了,再慢慢把表拆到不同库。
核心总结: 模块边界重构,本质上是「把变化隔离在最小范围内」。你不需要一步到位拆成微服务,但至少要让每个模块能独立修改、独立测试、独立部署。做到这三点,你的单体应用就已经具备了微服务的「魂」。
好了,这一章就聊到这儿。下一章咱们聊聊「重构中的测试策略」——怎么在改代码的时候,保证不改出bug。