重构案例实战(上):电商系统订单模块重构

从面条代码到领域驱动设计

说实话,我见过太多电商系统的订单模块了。几乎每个系统发展到一定阶段,订单模块都会变成一团乱麻。为什么?因为订单太核心了,每天都在加需求,每天都在改逻辑。改着改着,就成了一碗地道的「意大利面条」。

今天我就拿一个真实的案例,带你走一遍重构的全过程。这个案例是我去年帮一家中型电商公司做的,订单模块大概有3万行代码,全部堆在一个类里。嗯,你没看错,一个类,3万行。

核心问题:一个 OrderService 类,30000+ 行代码,包含订单创建、支付、发货、退款、售后等所有逻辑。方法之间互相调用,状态散落在各个角落。

第一步:识别坏味道

重构之前,我们先得知道问题在哪。我个人习惯先做代码评审,找出明显的坏味道。这个订单模块的问题非常典型:

  • 方法臃肿:一个 createOrder 方法 800 行,里面塞满了各种 if-else
  • 重复代码:订单状态判断的逻辑,在 20 多个方法里重复出现
  • 散弹式修改:加一个新支付方式,要改 5 个类、10 个方法
  • 全局变量泛滥:用了一堆 static 变量来存临时状态

我曾经在一个项目里见过更夸张的——一个方法里嵌套了 7 层 if-else,改一个条件要花半天时间理清所有分支。说白了,这种代码不改,迟早要出大问题。

第二步:梳理核心流程

重构不能上来就改代码。我建议先画一张图,把订单的核心流程理清楚。下面这张图是我当时画的:

订单核心流程(重构前 vs 重构后) 重构前:面条代码 OrderService(30000行) createOrder() payOrder() cancelOrder() refundOrder() shipOrder() confirmOrder() 重构后:领域驱动设计 领域层(Domain) Order OrderItem Payment 应用层(Application) OrderApplicationService PaymentApplicationService 基础设施层(Infrastructure) OrderRepository PaymentGateway

你看,左边是重构前的样子,所有逻辑混在一起,箭头乱飞。右边是重构后的分层结构,清晰多了。你想想看,如果让你维护左边的代码,你愿意吗?

第三步:识别聚合根

领域驱动设计里,最重要的概念就是聚合根。说白了,聚合根就是一组对象的「老大」,所有操作都要通过它来协调。

在订单模块里,聚合根很明显——就是 Order 本身。它下面有 OrderItem(订单项)、Payment(支付信息)、Shipping(物流信息)等实体。

我的经验:识别聚合根有个简单方法——看哪个对象「拥有」其他对象。订单拥有订单项,订单项不可能脱离订单存在。所以 Order 就是聚合根。

第四步:重构代码——从过程式到对象式

好了,理论说完了,我们来看实际代码。这是重构前的典型代码:

// 重构前:面条代码
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);
    }
}
注意:重构不是一蹴而就的。我曾经在一个项目里试图一次性重构整个模块,结果花了两个月,上线后出了十几个 bug。后来我学乖了——每次只重构一个子领域,用防腐层隔离新旧代码,逐步替换。

第五步:引入领域服务

有些逻辑不属于任何一个实体,比如「下单时检查用户是否有优惠券」。这时候就需要领域服务:

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 模式保证最终一致性
避坑指南:我曾经把库存扣减和订单创建放在同一个事务里,结果高并发时经常死锁。后来改成 Saga 模式,先创建订单,再异步扣减库存,如果扣减失败就取消订单。虽然复杂了一点,但系统稳定多了。

第七步:重构效果评估

重构完成后,我们做了个对比:

  • 代码行数:从 30000 行减少到 12000 行(减少了 60%)
  • 圈复杂度:从平均 25 降低到 8
  • 新增需求响应时间:从 3 天缩短到 0.5 天
  • 线上 Bug 数:从每月 15 个降低到 2 个

说实话,这个结果连我自己都没想到。重构之前,团队每天都在救火。重构之后,大家终于有时间做技术改进了。

总结

这次重构的核心思路,说白了就三句话:

  1. 识别聚合根:找到那个「老大」,所有操作通过它协调
  2. 拆分职责:把大方法拆成小方法,把大对象拆成小对象
  3. 分层隔离:领域层只关心业务,不关心技术细节

嗯,这次先讲到这里。下节课我会继续讲这个案例的下半部分——如何处理重构过程中的遗留系统、如何做渐进式重构、以及如何说服老板给你时间重构。这些都是实战中真正会遇到的问题。


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