重构与微服务:重构单体应用中的模块边界

说实话,微服务这个话题已经被聊烂了。但真正动手拆过单体应用的人,心里都清楚——拆服务不是技术问题,是边界问题。

我记得几年前接手过一个电商后台,代码堆了六七年,一个订单模块里混着库存扣减、优惠券核销、甚至还有用户积分。你想想看,这哪是模块?这分明是意大利面。当时团队想上微服务,结果一讨论,光「订单服务该不该管库存」就吵了两周。

所以这一章,我不跟你扯什么DDD理论。咱们就聊聊,怎么在实际代码里,把那些纠缠不清的模块边界,一刀一刀理清楚。

模块边界模糊的典型症状

先看看你的项目有没有这些「病」:

  • 跨模块直接调用内部方法——比如支付模块直接调了订单模块的私有DAO方法。
  • 共享数据库表——两个模块读写同一张表,改字段要两边一起改。
  • 循环依赖——A模块引了B模块,B模块又引了A模块。
  • 一个类干三件事——比如OrderService里同时处理订单、发短信、更新库存。

我在项目中遇到过最夸张的一个案例:一个UserService类,两千多行,里面混着登录、权限、消息推送、甚至还有文件上传。说白了,这就是「上帝类」——什么都能干,什么都耦合。

重构第一步:识别真正的业务边界

怎么找边界?我个人的习惯是:看变化频率

举个例子,订单的「创建」和「取消」经常变,但「支付回调」相对稳定。如果它们混在一个模块里,每次改订单逻辑都要重新测试支付回调,这就是边界没划好。

具体做法:

  1. 把每个模块的「变更日志」拉出来,看看哪些功能经常一起改。
  2. 把经常一起改的功能圈成一个「候选模块」。
  3. 检查这个候选模块对外暴露的接口——如果接口数量超过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访问其他模块的数据,不能直接查表。

物理隔离:等逻辑隔离稳定了,再把数据库拆开。比如订单模块的订单表,和支付模块的支付流水表,分到不同的数据库实例。

注意: 数据拆分时,一定要考虑「分布式事务」的问题。我曾经因为没处理好,导致一笔订单支付成功但库存没扣减,用户投诉了三天。后来用了「本地消息表+补偿机制」才解决。

核心逻辑:模块边界重构的决策树

下面这张图,是我在实际项目中总结的决策流程。每次遇到边界模糊的模块,我就拿它来走一遍:

模块边界模糊 功能是否经常一起变更? (查看近3个月变更日志) 合并为同一模块 是否有循环依赖? (检查import关系) 提取公共接口 是否共享数据库表? (检查表依赖) 拆分表+API隔离 模块边界清晰,可独立演进 迭代

这张图的逻辑其实很简单:先看变更频率,再看依赖关系,最后处理数据耦合。每一步都走通了,边界自然就清晰了。

避坑指南

最后分享几个我踩过的坑:

  • 不要为了微服务而微服务。 我曾经把一个只有三个方法的工具类拆成独立服务,结果引入网络延迟,性能反而下降了。模块边界重构的目标是「高内聚低耦合」,不是「拆得越碎越好」。
  • 接口设计要预留扩展点。 比如订单状态变更,不要只返回成功/失败,最好返回一个事件对象,让调用方自己决定怎么处理。这样以后加新逻辑,不用改接口。
  • 数据拆分要渐进。 别想着一天拆完。我习惯先做「读写分离」——读操作走缓存或视图,写操作走主库。等稳定了,再慢慢把表拆到不同库。

核心总结: 模块边界重构,本质上是「把变化隔离在最小范围内」。你不需要一步到位拆成微服务,但至少要让每个模块能独立修改、独立测试、独立部署。做到这三点,你的单体应用就已经具备了微服务的「魂」。

好了,这一章就聊到这儿。下一章咱们聊聊「重构中的测试策略」——怎么在改代码的时候,保证不改出bug。

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