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类 - 在
createOrder、cancelOrder、refundOrder里统一调用
这样,以后改库存规则,只改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. 核心逻辑图:重构前后的对比
下面这张图,展示了重构前后的核心变化。左边是“大泥球”,右边是“整洁架构”。
你看,重构前所有逻辑都挤在一个大方法里,像一团乱麻。重构后,每个职责都清晰独立,改一个地方不会影响其他地方。这就是重构的魅力。
6. 避坑指南
最后,分享几个我踩过的坑:
- 不要追求完美:重构不是重写。不要试图一次性把所有坏味道都消除。先解决最痛的那个。
- 测试先行:没有测试的重构,就像走钢丝。我吃过这个亏,现在每次重构前,先把测试覆盖率拉到80%以上。
- 小步快跑:每次提交只改一个坏味道。这样出了问题,回滚也容易。
好了,这一章我们识别了订单系统的坏味道,也制定了重构计划。下一章,咱们就动手开始拆那个800行的大方法。到时候,我会带着你一步步提取方法、引入参数对象。敬请期待。