21、实战案例一:电商订单系统重构(上)- 识别坏味道与设计重构计划

好,咱们正式开始第一个实战案例。

电商订单系统,这玩意儿我太熟了。几乎每个做后端开发的兄弟都会碰到。说白了,它就是整个电商系统的核心枢纽——用户下单、支付、库存扣减、物流发货,全得围着它转。

我这些年接手过的订单系统,少说也有七八个。每个都有各自的“历史遗留问题”。有的代码像一团乱麻,有的逻辑散落在各个角落。嗯,今天咱们就拿一个典型的“坏味道”订单系统开刀。

1. 现状分析:这个订单系统怎么了?

先看看这个系统的核心流程。用户下单后,系统要做这几件事:

  • 校验库存
  • 锁定库存
  • 生成订单
  • 扣减优惠券
  • 发送通知

听起来挺简单对吧?但实际代码里,这些逻辑全揉在一个createOrder()方法里。我见过最夸张的一个方法,足足有800行。你想想看,一个方法里既有SQL查询,又有远程调用,还有各种if-else判断。改一个地方,心惊胆战。

核心问题:订单创建方法承担了太多职责。它既要做数据校验,又要处理业务逻辑,还要协调外部服务。这违反了单一职责原则。

2. 识别坏味道:代码里的“定时炸弹”

我个人习惯,拿到一个系统先不急着改。先闻一闻,看看有哪些坏味道。这个订单系统,我闻到了以下几种:

2.1 霰弹式修改

什么叫霰弹式修改?就是改一个需求,你得跑遍好几个类去改代码。

举个例子,如果我们要新增一种支付方式(比如数字人民币),你得改:

  • 订单服务类(加一个if分支)
  • 支付回调处理类(加一个case)
  • 对账逻辑(加一个判断)
  • 甚至报表统计也得改

我曾经在一个项目里,为了加一个“拼团”功能,改了12个文件。改完之后,测试跑了三天,还是出了线上bug。这就是典型的霰弹式修改。

2.2 过长参数列表

看看这个方法的签名:

public Order createOrder(Long userId, Long productId, Integer quantity, 
                         Long addressId, String couponCode, Integer paymentType,
                         String remark, String source, String ip, 
                         String userAgent, ...)

我的天,十几个参数。每次调用都得小心翼翼,生怕传错顺序。而且,如果以后要加个“发票信息”,又得加两个参数。这方法迟早要爆炸。

2.3 重复代码

库存校验的逻辑,在createOrder()里有,在cancelOrder()里也有,甚至在refundOrder()里又出现了一遍。三个地方,逻辑一模一样,只是调用的时机不同。

你想想看,如果库存校验规则变了(比如增加了“预售商品”的判断),你得改三个地方。漏一个,就是bug。

2.4 依恋情结

一个方法,却对另一个类的数据特别感兴趣。比如订单服务里,大量操作了User对象的内部状态:

if (user.getVipLevel() > 3 && user.getRegisterDays() > 30) {
    // 给老用户打折
    order.setDiscount(0.9);
}

这种逻辑应该放在User类里,或者至少抽成一个策略。放在订单服务里,既破坏了封装,也让订单服务变得臃肿。

3. 重构计划:从哪里开始?

识别出坏味道之后,就要制定重构计划了。我建议分三步走:

步骤 目标 主要手法
第一步 拆分大方法 提取方法、提取类
第二步 消除重复代码 模板方法模式、策略模式
第三步 优化参数列表 引入参数对象

3.1 第一步:拆分大方法

先把那个800行的createOrder()拆开。我的做法是:

  • 把库存校验抽成validateStock()
  • 把订单生成抽成buildOrder()
  • 把优惠券处理抽成processCoupon()
  • 把通知发送抽成sendNotifications()

每个方法只做一件事。这样,主方法就变成了一个清晰的流程编排:

public Order createOrder(CreateOrderRequest request) {
    validateStock(request);
    Order order = buildOrder(request);
    processCoupon(request, order);
    sendNotifications(order);
    return order;
}

你看,是不是清爽多了?每个步骤都一目了然。以后改库存逻辑,只改validateStock()就行。

小技巧:拆分方法时,不要一次拆太细。我习惯先拆出5-8个方法,运行测试通过后,再考虑进一步拆分。步子太大容易扯着蛋。

3.2 第二步:消除重复代码

库存校验逻辑重复了三次。我的方案是:

  • 把库存校验逻辑封装成一个StockValidator
  • createOrdercancelOrderrefundOrder里统一调用

这样,以后改库存规则,只改StockValidator一个地方。测试也只需要测这一个类。

3.3 第三步:优化参数列表

那个十几个参数的方法,我引入了一个CreateOrderRequest对象:

public class CreateOrderRequest {
    private Long userId;
    private Long productId;
    private Integer quantity;
    private Long addressId;
    private String couponCode;
    private Integer paymentType;
    // ... 其他字段
}

这样,方法签名变成了:

public Order createOrder(CreateOrderRequest request)

以后加字段,只改CreateOrderRequest类,不用改方法签名。而且,参数校验也可以放在这个类里,一举两得。

4. 重构路线图:先稳后快

重构最忌讳的就是“大干快上”。我见过一个团队,重构到一半,业务方突然要加个紧急需求,结果重构和需求搅在一起,代码越改越乱。

我的建议是:

  • 先加测试:没有测试的重构就是耍流氓。先把核心流程的测试用例补上。
  • 小步提交:每次只改一个坏味道,改完就跑测试。测试通过再提交。
  • 不要混合:重构期间,不要加新功能。重构就是重构,新功能就是新功能。

注意:我曾经在一个重构项目里,一边改代码一边加新需求。结果重构完,线上出了三个bug,全是新需求引入的。从那以后,我定了个规矩:重构期间,只改结构,不改行为。

5. 核心逻辑图:重构前后的对比

下面这张图,展示了重构前后的核心变化。左边是“大泥球”,右边是“整洁架构”。

重构前后对比:从大泥球到整洁架构 重构前:大泥球 createOrder() 800行 校验库存 | 生成订单 | 扣优惠券 | 发通知 重复代码:库存校验出现3次 长参数列表:12个参数 依恋情结:大量操作User内部状态 重构后:整洁架构 createOrder() 流程编排 validateStock → buildOrder → processCoupon → send 单一职责:每个方法只做一件事 StockValidator | OrderBuilder | CouponService 参数对象:CreateOrderRequest 封装所有参数,易于扩展 消除重复:StockValidator统一校验

你看,重构前所有逻辑都挤在一个大方法里,像一团乱麻。重构后,每个职责都清晰独立,改一个地方不会影响其他地方。这就是重构的魅力。

6. 避坑指南

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

  • 不要追求完美:重构不是重写。不要试图一次性把所有坏味道都消除。先解决最痛的那个。
  • 测试先行:没有测试的重构,就像走钢丝。我吃过这个亏,现在每次重构前,先把测试覆盖率拉到80%以上。
  • 小步快跑:每次提交只改一个坏味道。这样出了问题,回滚也容易。

好了,这一章我们识别了订单系统的坏味道,也制定了重构计划。下一章,咱们就动手开始拆那个800行的大方法。到时候,我会带着你一步步提取方法、引入参数对象。敬请期待。


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