找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1928

积分

0

好友

252

主题
发表于 昨天 00:43 | 查看: 2| 回复: 0

不知道你有没有遇到过这种情况:一个订单创建的接口,刚上线的时候只有几十行代码,逻辑也很简单。但随着业务迭代,三个月后代码就变得臃肿不堪:

public void createOrder(OrderRequest request) {
    // 参数校验
    if (request.getUserId() == null) {
        throw new IllegalArgumentException("用户ID不能为空");
    }
    // ... 省略 20 行校验代码

    // 权限校验
    if (!checkPermission(request.getUserId())) {
        throw new ForbiddenException("无权限");
    }

    // 业务校验
    if (getOrderCount(request.getUserId()) >= 10) {
        throw new BusinessException("订单数量超限");
    }
    // ... 省略 15 行业务校验

    // 创建订单
    Order order = buildOrder(request);
    orderMapper.insert(order);

    // 写操作日志
    try {
        operateLogService.log(order.getId(), "CREATE");
    } catch (Exception e) {
        // 日志失败不影响主流程
        log.error("写日志失败", e);
    }

    // 发送通知
    try {
        notificationService.send(order);
    } catch (Exception e) {
        log.error("发送通知失败", e);
    }

    // 异步风控检查
    asyncRiskCheckService.check(order);
}

几百行代码里,iftry-catch 到处都是。更让人头疼的是后续的维护问题:

  • 日志写失败要不要抛异常?
  • 通知发送失败要不要回滚订单?
  • 新增一个“优惠券核销”逻辑该加在哪里?
  • 测试环境想关闭风控检查又该怎么处理?

问题出在哪?

仔细分析上面的代码,它实际想完成的任务可以清晰地拆分为以下几个步骤:

1. 校验参数
2. 校验权限
3. 校验业务规则
4. 创建订单(核心)
5. 记录日志
6. 发送通知
7. 风控检查

这些步骤各有特点:

  • 有的必须成功:比如参数校验、订单创建
  • 有的可以失败:比如日志记录、通知发送
  • 有的同步执行:比如各种校验
  • 有的异步执行:比如风控检查
  • 有的可能需要关闭:比如测试环境不需要风控

但传统的写法将所有逻辑都混杂在同一个方法里。步骤的顺序、失败后的处理策略、以及某个步骤是否可选,全都依赖代码中零散的 iftry-catch 来控制,代码自然会变得混乱且难以维护。

有没有更好的写法?

如果我们将每个步骤抽象成一个独立的“节点”,再用一个“管道”把它们有序地串联起来执行,情况会怎样?

首先,定义一个节点接口:

public interface PipelineNode<T> {
    void execute(PipelineContext<T> context);
}

每个节点只专注于完成一件具体的事情,它不关心前后节点是谁,也不关心自己的执行序号。

然后,定义一个管道来负责调度和执行这些节点:

public class ExecutionPipeline<T> {

    private final List<PipelineNode<T>> nodes;

    public void execute(T data) {
        PipelineContext<T> ctx = new PipelineContext<>(data);
        for (PipelineNode<T> node : nodes) {
            if (ctx.isInterrupted()) {
                break;
            }
            node.execute(ctx);
        }
    }
}

现在,我们来看看用这种模式重写订单创建会是怎样的体验:

Pipeline pipeline = Pipeline.builder()
    .add(new ParamValidateNode())       // 参数校验
    .add(new PermissionCheckNode())     // 权限校验
    .add(new BusinessValidateNode())    // 业务校验
    .add(new CreateOrderNode())         // 创建订单
    .add(new OperateLogNode())          // 写日志
    .add(new NotificationNode())        // 发通知
    .add(new AsyncRiskCheckNode())      // 风控检查
    .build();

pipeline.execute(orderRequest);

看,主流程的核心代码被压缩到了寥寥数行,每一个业务逻辑都被封装在独立的、职责单一的节点中,清晰度大大提升。

这样做有什么好处?

1. 节点可插拔,灵活配置

想要新增一个“优惠券核销”逻辑?只需增加一个节点即可:

.add(new CouponDeductNode())

想根据环境动态开关某个功能(比如风控)?可以通过配置轻松控制:

.add(env.isProd() ? new RiskCheckNode() : new SkipNode())

2. 失败策略清晰明确

我们可以在 PipelineContext 中为每个节点定义清晰的失败处理策略:

public enum FailureStrategy {
    CONTINUE,    // 失败后继续执行后续节点
    STOP,        // 失败后中断整个管道
    ROLLBACK     // 失败后执行回滚(需配合事务管理)
}

例如,日志节点的失败策略可以设为 CONTINUE(日志失败不影响订单创建),而订单创建节点的失败策略则设为 STOP(创建失败则流程终止)。策略一目了然,不再需要散落的 try-catch 去判断。

3. 节点高度可复用

像参数校验、权限校验、操作日志记录这样的节点,其逻辑是通用的,可以被轻松复用到其他业务管道中,避免重复编码。

4. 执行顺序集中管控

节点的执行顺序完全由管道在组装时定义,每个节点内部无需关心顺序逻辑,这使得流程的编排和控制变得集中且直观。

执行管道:一种轻量级的工程化方案

上面介绍的方案,本质上是一种 执行管道(Execution Pipeline) 设计模式。它并非一个特定的框架,而是一种轻量级的工程设计思想:将一次业务执行拆分为多个可插拔、有明确顺序、且失败策略可控的阶段,并通过统一的机制将其串联执行。

它跟 AOP、责任链有什么区别?

你可能会联想到 AOP、责任链、过滤器等工作流引擎。它们确有相似之处,但侧重点不同:

方案 适用场景 局限性
AOP 通用的横切逻辑(如日志、事务) 执行顺序不直观,难以做精细的流程控制和编排
Filter/Interceptor HTTP 请求级别的预处理和后处理 通常局限于Web请求处理,难以用于普通业务方法
@Async 简单的异步执行 只解决并发问题,不解决流程的编排与步骤管理
责任链模式 需要动态组合处理链的场景 更偏重模式本身,缺少开箱即用的工程化封装(如上下文、失败策略)
工作流引擎 跨系统、复杂且多变的业务流程 过于重型,对于单机应用内的流程编排来说,引入和维护成本较高

相比之下,执行管道可以看作是“工程化封装的责任链模式”。它汲取了责任链的思想,并在此基础上增加了上下文传递、统一失败策略、节点生命周期管理等工程实践,专门用于优雅地解决单机应用内部的复杂业务流程编排问题。

什么时候该考虑使用执行管道?

并非所有场景都需要引入执行管道。如果你的业务逻辑步骤固定、非常简单,或者流程虽然复杂但长期稳定不变,那么直接编码可能是更直接的选择。

但是,当你的代码出现以下“信号”时,就值得考虑引入执行管道模式了:

  • 某个核心方法变得过长(超过百行),阅读和维护困难。
  • 流程中包含多个“可选”步骤或“异步”执行步骤。
  • 业务需要频繁地插入新的处理逻辑(如各种校验、钩子)。
  • 不同运行环境(如测试、生产)需要不同的执行流程。

执行管道特别适合管理单机应用内的复杂核心业务流程,例如订单创建、用户注册、数据导入/导出、计费核算等场景。

重构建议与资源

优秀的代码架构往往是在持续重构中演进出来的,而非一蹴而就。如果你的项目中已经存在类似的“巨无霸”方法,不必急于全盘推翻。可以尝试从其中剥离出一个最独立的逻辑,将其改造成第一个管道节点,然后逐步迁移,让代码朝着清晰、可控的方向进化。

文中提到的执行管道设计模式,在 Spring Boot 项目中可以很好地与IoC容器结合,通过依赖注入来管理节点Bean。同时,这种对流程进行清晰拆解和编排的思想,也属于广义的 Pipeline 实践,值得深入探索。

对于想动手实践的开发者,可以参考示例代码仓库:

https://github.com/yuboon/java-examples/tree/master/springboot-pipeline

希望这种模式能帮助你更好地组织代码,让复杂的业务流程变得清晰、优雅且易于维护。在 云栈社区 也有更多关于架构设计和代码实践的讨论,欢迎交流。




上一篇:Istio VirtualService 路由配置详解:从匹配规则到金丝雀发布与超时重试策略
下一篇:Redis高并发支撑原理剖析:单线程、内存操作与IO多路复用
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-12 01:09 , Processed in 0.643533 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表