重构案例实战(上):电商系统订单模块重构
从面条代码到领域驱动设计
说实话,我见过太多电商系统的订单模块了。几乎每个系统发展到一定阶段,订单模块都会变成一团乱麻。为什么?因为订单太核心了,每天都在加需求,每天都在改逻辑。改着改着,就成了一碗地道的「意大利面条」。
今天我就拿一个真实的案例,带你走一遍重构的全过程。这个案例是我去年帮一家中型电商公司做的,订单模块大概有3万行代码,全部堆在一个类里。嗯,你没看错,一个类,3万行。
第一步:识别坏味道
重构之前,我们先得知道问题在哪。我个人习惯先做代码评审,找出明显的坏味道。这个订单模块的问题非常典型:
- 方法臃肿:一个 createOrder 方法 800 行,里面塞满了各种 if-else
- 重复代码:订单状态判断的逻辑,在 20 多个方法里重复出现
- 散弹式修改:加一个新支付方式,要改 5 个类、10 个方法
- 全局变量泛滥:用了一堆 static 变量来存临时状态
我曾经在一个项目里见过更夸张的——一个方法里嵌套了 7 层 if-else,改一个条件要花半天时间理清所有分支。说白了,这种代码不改,迟早要出大问题。
第二步:梳理核心流程
重构不能上来就改代码。我建议先画一张图,把订单的核心流程理清楚。下面这张图是我当时画的:
你看,左边是重构前的样子,所有逻辑混在一起,箭头乱飞。右边是重构后的分层结构,清晰多了。你想想看,如果让你维护左边的代码,你愿意吗?
第三步:识别聚合根
领域驱动设计里,最重要的概念就是聚合根。说白了,聚合根就是一组对象的「老大」,所有操作都要通过它来协调。
在订单模块里,聚合根很明显——就是 Order 本身。它下面有 OrderItem(订单项)、Payment(支付信息)、Shipping(物流信息)等实体。
第四步:重构代码——从过程式到对象式
好了,理论说完了,我们来看实际代码。这是重构前的典型代码:
// 重构前:面条代码
public class OrderService {
public void createOrder(Long userId, List<Long> productIds,
List<Integer> quantities, String address) {
// 800 行代码...
// 校验用户
// 校验库存
// 计算价格
// 生成订单号
// 保存订单
// 扣减库存
// 发送通知
// ... 全部混在一起
}
public void payOrder(Long orderId, String paymentMethod) {
// 500 行代码...
// 查询订单
// 校验订单状态
// 调用支付网关
// 更新订单状态
// 发送支付成功通知
}
}
重构后,我们把逻辑拆分到不同的领域对象里:
// 重构后:领域驱动设计
public class Order {
private OrderId orderId;
private OrderStatus status;
private List<OrderItem> items;
private Payment payment;
private Shipping shipping;
// 订单自己负责状态变更
public void pay(PaymentMethod method) {
// 校验状态
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态不允许支付");
}
// 创建支付对象
this.payment = new Payment(method, this.calculateTotal());
// 变更状态
this.status = OrderStatus.PAID;
// 记录领域事件
this.addDomainEvent(new OrderPaidEvent(this.orderId));
}
public void ship(String trackingNumber) {
if (this.status != OrderStatus.PAID) {
throw new IllegalStateException("订单未支付,无法发货");
}
this.shipping = new Shipping(trackingNumber);
this.status = OrderStatus.SHIPPED;
this.addDomainEvent(new OrderShippedEvent(this.orderId, trackingNumber));
}
// 计算总价
private Money calculateTotal() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
第五步:引入领域服务
有些逻辑不属于任何一个实体,比如「下单时检查用户是否有优惠券」。这时候就需要领域服务:
public class OrderDomainService {
private final CouponRepository couponRepository;
private final InventoryService inventoryService;
public Order createOrder(User user, List<OrderItemRequest> items) {
// 检查库存
for (OrderItemRequest item : items) {
if (!inventoryService.hasStock(item.getProductId(), item.getQuantity())) {
throw new InsufficientStockException(item.getProductId());
}
}
// 计算优惠
Coupon coupon = couponRepository.findByUserId(user.getId());
Money discount = coupon != null ? coupon.calculateDiscount(items) : Money.ZERO;
// 创建订单
Order order = new Order(user, items, discount);
// 应用优惠券
if (coupon != null) {
coupon.apply();
couponRepository.save(coupon);
}
return order;
}
}
你发现没有?领域服务不持有状态,它只是协调各个领域对象完成一个业务操作。说白了,它就是个「组织者」。
第六步:处理事务边界
重构过程中,事务处理是个大坑。原来的面条代码里,一个方法就是一个事务,简单粗暴。但拆分成多个领域对象后,事务边界就变得模糊了。
| 场景 | 重构前 | 重构后 |
|---|---|---|
| 创建订单 | 一个事务包含所有操作 | 应用层控制事务,领域层不关心 |
| 支付回调 | 直接在 Controller 里写事务 | 通过领域事件异步处理 |
| 库存扣减 | 和订单创建在同一个事务 | 使用 Saga 模式保证最终一致性 |
第七步:重构效果评估
重构完成后,我们做了个对比:
- 代码行数:从 30000 行减少到 12000 行(减少了 60%)
- 圈复杂度:从平均 25 降低到 8
- 新增需求响应时间:从 3 天缩短到 0.5 天
- 线上 Bug 数:从每月 15 个降低到 2 个
说实话,这个结果连我自己都没想到。重构之前,团队每天都在救火。重构之后,大家终于有时间做技术改进了。
总结
这次重构的核心思路,说白了就三句话:
- 识别聚合根:找到那个「老大」,所有操作通过它协调
- 拆分职责:把大方法拆成小方法,把大对象拆成小对象
- 分层隔离:领域层只关心业务,不关心技术细节
嗯,这次先讲到这里。下节课我会继续讲这个案例的下半部分——如何处理重构过程中的遗留系统、如何做渐进式重构、以及如何说服老板给你时间重构。这些都是实战中真正会遇到的问题。