接手一个被 if-else 淹没的订单处理模块

去年秋天我接了一个外包团队的遗留项目。代码库不算老,Laravel 8 写的,但订单处理那块简直是个灾难。一个方法里塞了十几层 if-else,判断支付方式、会员等级、优惠券类型、库存状态,甚至还有地理位置。每次加一个新促销规则,整个团队都得屏住呼吸。

单元测试覆盖率不到 10%。不是不想写,是真没法写。你想 mock 一个方法,先得把里面二十几个条件分支全搞清楚。有一次改了个优惠券的优先级,结果把库存扣减逻辑带崩了,线上出了两小时故障。

那段时间我每次打开那个文件都头疼。

从中间件启发到逻辑拆分

后来翻 Laravel 官方文档,看到 Pipeline 那章。Laravel 本身就在很多地方用了它——比如中间件就是典型的管道模式。每个请求经过一堆中间件,每个中间件只干一件事:验证、日志、会话、CSRF。这不就是我们想要的吗?

Pipeline 的核心思想极其简单:数据流经一串步骤,每个步骤只处理自己关心的那部分,然后传给下一个。没有全局状态,没有隐式依赖。每个步骤的 handle 方法接收数据,做点事,return 回去。仅此而已。

对比传统做法,差异很明显。以前改一个条件分支可能要翻三页代码,现在想改支付验证,直接进 PaymentValidationPipe 那个文件就行。每个步骤可以单独测、单独部署,甚至单独扔掉重写。

Laravel Pipeline pipes data flow

实战:将订单处理拆解为管道中的步骤

我开始动手重构。先理清订单处理到底要经历哪些阶段:支付方式校验、会员折扣计算、优惠券应用、库存扣减、订单生成。每个阶段就是一个 pipe 类。比如会员折扣那步:

class MemberDiscountPipe
{
    public function handle($order, $next)
    {
        $discount = $order->user->getMemberDiscountRate();
        $order->total *= (1 - $discount);
        return $next($order);
    }
}

然后用 Pipeline 把它们串起来:

Pipeline::send($order)
    ->through([
        PaymentValidationPipe::class,
        MemberDiscountPipe::class,
        CouponApplicationPipe::class,
        StockCheckPipe::class,
        OrderGenerationPipe::class,
    ])
    ->then(function ($order) {
        return $order->save();
    });

via 方法可以指定传递的模型类型,让类型提示更精准。每个步骤只管自己的事,不再需要关心前面步骤做了什么——数据在管道里流动,每个步骤只拿到当前状态。这感觉就像把一团乱麻拆成五根绳子,每根绳子自己捋直了。

developer refactoring order pipeline

中途踩坑:步骤间依赖和异常处理

理想很丰满,现实有坑。第一个坑是步骤间耦合。会员折扣算出结果后,优惠券步骤需要知道用了多少折扣——但管道默认只传订单对象。解决方案是在订单对象上加个 appledDiscount 属性,每个步骤往里写元数据。不是最优解,但比全局变量强。

第二个坑是异常处理。某个步骤失败了怎么办?比如库存不足,总不能继续生成订单。我开始用 then 方法里抛异常,但后来发现 Pipeline 自带 via 和 pipe 方法可以自定义异常流程。写了个 StockCheckException,一旦库存步骤抛出它,直接在管道里捕获并回滚。

Pipeline::send($order)
    ->through($pipes)
    ->then(function ($order) {
        try {
            $order->save();
        } catch (OrderProcessingException $e) {
            // 回滚库存、释放优惠券
            $this->rollback($order, $e);
        }
    });

还有个头疼事:业务方临时要加个“满减阶梯”活动。以前得改主方法,现在只需写个 LadderDiscountPipe,插到管道中间。通过 pipe 方法动态添加,完全不影响其他步骤。

重构后的代码:可读性、可测试性、可扩展性全面提升

重构完一看,代码量少了大概 40%。不是魔法,是大量重复的条件判断被合并到每个步骤自己的逻辑里了。每个文件七八十行,一个类只干一件事。

单元测试覆盖率从不到 10% 飙到 90%。每个 pipe 独立测试,传个 mock 订单进去,断言输出。测试速度和开发速度都上来了。最爽的是加新功能——比如“黑五限时折扣”,写个新 pipe,插到优惠券后面,跑一遍现有测试,全绿。改一行配置就能上线。

现在团队新人看订单处理代码,不再需要从头到尾读一遍那几百行的条件分支。看一眼 pipes 数组就知道流程。出问题定位也快——先看哪个 pipe 抛异常,进去排查就好。

Laravel Pipeline 不是什么新东西,中间件模式早就在用。但很多人只把它当中间件用,没想到业务逻辑也能这么拆。如果你也在跟一堆 if-else 死磕,不妨试试。反正我是不想再回去改那个二百行的订单函数了。一次都不想。