责任链模式:审批流程——让每个节点都有机会说“不”

大家好,我是你们的老朋友。今天我们来聊聊责任链模式。

说实话,这个模式在业务系统里太常见了。你想想看,但凡涉及到“逐级审批”、“层层过滤”的场景,十有八九就是责任链的用武之地。我最早接触它,是在做一个OA系统的请假模块。那时候刚入行,代码写得那叫一个“直来直去”——if-else 嵌套了七八层,组长审批完判断是否要经理审批,经理审批完再判断是否要总监审批……后来代码改得我头皮发麻,才意识到:嗯,该用责任链了。

什么是责任链模式?

责任链模式,说白了就是:把多个处理对象串成一条链,请求沿着链传递,直到有对象处理它为止

它的核心思想是:解耦请求的发送者和接收者。发送者不需要知道谁会处理这个请求,也不需要知道处理顺序。它只管把请求丢出去,剩下的交给链上的节点。

核心角色:

  • Handler(抽象处理者):定义处理请求的接口,并持有下一个处理者的引用。
  • ConcreteHandler(具体处理者):实现自己的处理逻辑,如果自己能处理就处理,否则交给下一个。
  • Client(客户端):组装链,并触发请求。

业务场景:OA系统中的请假审批

咱们直接上场景。假设公司请假审批规则是这样的:

  • 请假天数 ≤ 3 天:组长审批即可
  • 3 天 < 天数 ≤ 7 天:组长审批后,需要经理审批
  • 7 天 < 天数 ≤ 15 天:组长 → 经理 → 总监
  • 天数 > 15 天:组长 → 经理 → 总监 → CEO

你看,这就是一条典型的责任链。每个节点都有机会处理,处理不了就往后传。

代码实现

我习惯先定义一个抽象的处理者。注意,这里要持有下一个节点的引用:

// 抽象处理者
public abstract class Approver {
    protected Approver nextApprover;  // 下一个审批人

    public void setNextApprover(Approver nextApprover) {
        this.nextApprover = nextApprover;
    }

    // 模板方法:处理请求
    public void handleRequest(LeaveRequest request) {
        if (canApprove(request)) {
            doApprove(request);
        } else if (nextApprover != null) {
            nextApprover.handleRequest(request);
        } else {
            System.out.println("无人能审批,请求被拒绝");
        }
    }

    protected abstract boolean canApprove(LeaveRequest request);
    protected abstract void doApprove(LeaveRequest request);
}

然后定义具体的审批人。每个节点只关心自己能处理的范围:

// 组长
public class GroupLeader extends Approver {
    @Override
    protected boolean canApprove(LeaveRequest request) {
        return request.getDays() <= 3;
    }

    @Override
    protected void doApprove(LeaveRequest request) {
        System.out.println("组长审批通过:" + request.getName() + " 请假 " + request.getDays() + " 天");
    }
}

// 经理
public class Manager extends Approver {
    @Override
    protected boolean canApprove(LeaveRequest request) {
        return request.getDays() <= 7;
    }

    @Override
    protected void doApprove(LeaveRequest request) {
        System.out.println("经理审批通过:" + request.getName() + " 请假 " + request.getDays() + " 天");
    }
}

// 总监
public class Director extends Approver {
    @Override
    protected boolean canApprove(LeaveRequest request) {
        return request.getDays() <= 15;
    }

    @Override
    protected void doApprove(LeaveRequest request) {
        System.out.println("总监审批通过:" + request.getName() + " 请假 " + request.getDays() + " 天");
    }
}

// CEO
public class CEO extends Approver {
    @Override
    protected boolean canApprove(LeaveRequest request) {
        return true;  // CEO 啥都能批
    }

    @Override
    protected void doApprove(LeaveRequest request) {
        System.out.println("CEO审批通过:" + request.getName() + " 请假 " + request.getDays() + " 天");
    }
}

客户端组装链,发起请求:

public class Client {
    public static void main(String[] args) {
        // 组装责任链
        Approver groupLeader = new GroupLeader();
        Approver manager = new Manager();
        Approver director = new Director();
        Approver ceo = new CEO();

        groupLeader.setNextApprover(manager);
        manager.setNextApprover(director);
        director.setNextApprover(ceo);

        // 发起请求
        LeaveRequest request = new LeaveRequest("张三", 10);
        groupLeader.handleRequest(request);
    }
}

输出结果:

组长审批通过:张三 请假 10 天
经理审批通过:张三 请假 10 天
总监审批通过:张三 请假 10 天

等等,这里有个问题。你看输出,组长、经理、总监都审批了?这不对啊!

嗯,这里要特别注意。我上面的代码是每个节点都处理了,但实际业务中,一个请求通常只被一个节点处理。比如请假10天,组长审批完,应该直接交给经理,组长自己不应该“通过”。

所以正确的做法是:每个节点处理完自己的部分后,如果还需要上级审批,就继续往下传;如果不需要了,就终止。或者更常见的做法是:每个节点只判断自己能不能批,能批就批,不能批就传,而不是“批了还传”。

避坑指南: 我曾经在一个项目中,把责任链写成了“每个节点都处理一遍”,结果一个请假单被四个人审批了四次。后来才发现,责任链的典型用法是“找到一个能处理的就停止”,除非你的业务明确要求“逐级审批且每级都要签字”。

所以,正确的逻辑应该是:如果当前节点能处理,就处理并结束;否则传给下一个。修改一下 handleRequest 方法:

public void handleRequest(LeaveRequest request) {
    if (canApprove(request)) {
        doApprove(request);
    } else if (nextApprover != null) {
        nextApprover.handleRequest(request);
    } else {
        System.out.println("无人能审批,请求被拒绝");
    }
}

这样,请假10天,组长不能批(因为超过3天),传给经理;经理不能批(超过7天),传给总监;总监能批(≤15天),审批通过,结束。

另一个经典场景:Java Web 中的 Filter 链

说到责任链,Java Web 开发者肯定不陌生。Servlet 中的 Filter 就是典型的责任链模式。

你想想看,一个请求进来,经过多个 Filter:

  • 第一个 Filter 做字符编码处理
  • 第二个 Filter 做登录校验
  • 第三个 Filter 做日志记录
  • ……

每个 Filter 处理完自己的逻辑后,调用 chain.doFilter() 把请求传给下一个 Filter。这就是责任链的变体——每个节点都处理,但处理完后继续传递

我个人习惯把这种叫做“全链处理型”,而前面的审批流程叫做“单节点处理型”。两种都是责任链,只是终止条件不同。

责任链模式的核心流程图

下面我用一张 SVG 图来展示责任链的核心逻辑。这张图我画的是“单节点处理型”,也就是审批流程那种:

责任链模式核心流程(单节点处理型) 客户端发起请求 节点1:组长(能否处理?) ✅ 处理并结束 ❌ 传给下一节点 → 节点2(经理) 图例 能处理 → 结束 不能处理 → 传递

责任链模式的优缺点

优点 缺点
1. 解耦请求发送者和接收者 1. 请求可能不被处理(链尾无人处理)
2. 可以动态调整链的顺序和节点 2. 调试时不太直观,链太长影响性能
3. 符合开闭原则,新增节点不影响现有代码 3. 容易形成“过长的链”,维护成本上升
4. 每个节点职责单一,便于测试 4. 如果链的组装逻辑复杂,容易出错

实际项目中的经验

我在项目中用过责任链模式的地方还挺多的,除了审批流程,还有:

  • 数据校验链:非空校验 → 格式校验 → 业务规则校验 → 持久化
  • 日志过滤链:敏感词过滤 → 日志级别过滤 → 输出格式处理
  • 支付风控链:黑白名单检查 → 频率限制 → 金额限制 → 风控模型评分

我个人习惯把责任链的节点设计成无状态的,这样每个节点可以复用,也方便单元测试。如果节点需要状态,我会用 ThreadLocal 或者请求上下文来传递,而不是把状态挂在节点上。

小技巧: 如果你用 Spring 框架,可以把每个处理者注册成 Bean,然后用 List 注入,动态组装链。这样新增一个审批节点,只需要加一个类,改一下配置,不用改任何代码。

什么时候不该用责任链?

责任链虽好,但别滥用。我见过有人把简单的 if-else 也硬套成责任链,结果代码反而更复杂了。

我的建议是:如果处理节点不超过3个,且逻辑固定不变,直接用 if-else 更清晰。责任链的优势在于“动态扩展”和“解耦”,如果这两个需求都不存在,就别强行上模式。

另外,如果链太长(超过10个节点),性能会受影响。每个请求都要遍历一遍链,虽然每个节点逻辑简单,但积少成多。这时候可以考虑用“责任链树”或者“策略模式+责任链”来优化。

总结

责任链模式,说白了就是“让每个节点都有机会处理请求,但谁都不强制处理”。它把请求的发送者和接收者彻底解耦,让系统更灵活、更容易扩展。

你在写审批流程、过滤器、拦截器、校验链的时候,都可以想想责任链。但记住:模式是工具,不是目的。用对了,代码清爽;用错了,反而添乱。

好了,今天的内容就到这里。希望你在实际项目中能用上这个模式,少踩一些我当年踩过的坑。


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