设计模式预计阅读 15 分钟5950 字3708 次浏览

设计模式修炼之路 ⑰ 责任链模式——请求沿链条传递

客户端只认识链头、节点自治三段式、纯链与管线式、从 Servlet Filter 到 Netty Pipeline

目录

行为型第四站。

上一篇结尾留了个场景:报销单从你手里递出去,组长批不过找经理,经理批不过找总监,请求沿着链条一站一站传下去,谁有权限谁处理。

这一篇就把这套"击鼓传花"讲透——责任链模式,审批流的经典实现,也是各大框架的骨架模式。

一、责任链模式是什么

GoF 的原始定义:

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it. (让多个对象都有机会处理请求,避免请求的发送者和接收者耦合。把接收对象串成一条链,让请求沿着链传递,直到有对象处理它为止。)

翻译成人话:把一串处理者连成链,请求进来只递给第一个;每个处理者自己决定——在我手里就处理,不在我手里就传给下家。

三个角色:

角色 干什么 本文对应
Handler(抽象处理者) 定义处理接口 + 持有下家的引用 Approver
ConcreteHandler(具体处理者) 自己的权限范围内处理,范围外转交下家 组长/经理/总监/财务总监
Client(客户端) 组链 + 把请求交给链头 ChainTest

类比就是报销审批本身:你(Client)只把单子递给直属组长(链头),组长翻一下额度——300 块,自己就签了;80 万?签不动,转给经理。你从不需要知道整条审批线长什么样,你只认识你组长。

和前两篇串起来看,行为型已经凑齐了"分派三部曲":⑮ 策略解决"选哪个算法",⑯ 观察者解决"通知哪些下游",⑰ 责任链解决"请求谁接盘"——前两个的决策权在调用方/名单,这一个的决策权下放到每个节点自己手里。

二、优点与缺点

优点:

  1. 发送者只认识链头。客户端不知道有几个处理者、怎么路由,耦合度降到最低。
  2. 节点自治。每个处理者只关心"我的额度是多少、批还是转",节点之间互不认识——除了自己的下家。
  3. 动态组链。链条是运行时拼的:加一级审批、临时抽掉一个环节、换个顺序,都是改一行组装代码的事,节点类不动。
  4. 符合开闭原则。新加一个 VP 审批层 = 写一个类 + 组链时插进去。

缺点:

  1. 请求可能没人处理。链走到头还是没人接,请求就"落空"了——本例 200 万报销单就走到了链尾。必须设计好兜底(驳回、默认处理、或者日志告警),否则请求无声消失,是生产事故级 bug。
  2. 调试费劲。一个请求在链上跳来跳去,出问题时要在每个节点打断点、看日志,不如一个 if-else 直观。
  3. 性能损耗。权限明明在总监手里,请求还是要从组长、经理白跑两站。链越长、命中越靠后,浪费越多。
  4. 顺序敏感。组链的人把总监排在组长前面,整条链的逻辑就乱了。链条的正确性靠组装代码保证,编译器帮不了你。

三、什么时候用

判断标准:同一个请求,有多个候选处理者,该谁处理要由"请求内容 + 处理者能力"在运行时决定,且发送者不该知道路由规则。

  • 层级审批:报销、请假、合同会签——本文场景
  • 规则拦截:风控系统里"黑名单→限频→额度"层层筛查,谁先拦住谁处理
  • 请求预处理管线:编码、鉴权、日志、防攻击……每个环节处理自己的部分再放行(见第五节 Servlet Filter)
  • 事件传递:GUI 里的事件冒泡,子控件不处理就往父控件传

反面对照:处理者唯一且已知——直接调用;处理者之间要全跑一遍——那是观察者(⑯)的广播;需要所有分支并列可选——那是策略(⑮)。

四、实现方式:把路由权下放给节点

4.1 反面教材:提交人自己路由

public class BadExpenseSystem {

    public void submit(double amount) {
        if (amount <= 1000) {
            System.out.println("  [组长] 批准报销 " + amount + " 元");
        } else if (amount <= 10000) {
            System.out.println("  [经理] 批准报销 " + amount + " 元");
        } else if (amount <= 100000) {
            System.out.println("  [总监] 批准报销 " + amount + " 元");
        } else if (amount <= 1000000) {
            System.out.println("  [财务总监] 批准报销 " + amount + " 元");
        } else {
            System.out.println("  [无人能批] 金额过大,提交董事会讨论");
        }
        // 公司新设一个 VP 审批层?各级额度调整?
        // 这里全得改,而且所有要审批的地方都得复制一遍这套路由规则
    }
}

又是 if-else 巨无霸?看着像 ⑮ 的病,其实病灶不同:⑮ 的分支是并列的算法,这里的分支是层级路由——它的问题不是 if-else 本身,而是"路由规则"被焊死在了提交人这边。公司调整额度、新设层级,所有提交审批的代码全要改一遍。

4.2 正解:Handler 拿着下家引用,各自为政

第一步,抽象处理者——核心就两个字段一个方法:

public abstract class Approver {

    protected final String name;
    protected Approver next;          // 下一个处理者

    public Approver(String name) {
        this.name = name;
    }

    // 组链:返回下一个节点,方便链式调用
    public Approver setNext(Approver next) {
        this.next = next;
        return next;
    }

    // 处理请求:自己处理,或者传给下家
    public abstract void approve(double amount);
}

第二步,每个具体处理者实现同一套决策逻辑——权限内处理,权限外转交:

public class LeaderApprover extends Approver {

    public LeaderApprover(String name) {
        super(name);
    }

    @Override
    public void approve(double amount) {
        if (amount <= 1000) {
            System.out.println("  [组长" + name + "] 批准报销 " + amount + " 元");
        } else if (next != null) {
            System.out.println("  [组长" + name + "] " + amount + " 元超出权限,转交下一级");
            next.approve(amount);
        } else {
            System.out.println("  [组长" + name + "] 我是链条最后一环,无人能批,驳回");
        }
    }
}

ManagerApprover(≤1 万)、DirectorApprover(≤10 万)、CfoApprover(≤100 万)结构完全相同,只是阈值和称谓不同。

第三步,客户端组链、递交:

public class ChainTest {

    public static void main(String[] args) {
        System.out.println("========== 反面教材:提交人自己路由 ==========");
        BadExpenseSystem bad = new BadExpenseSystem();
        bad.submit(300);
        bad.submit(8000);
        bad.submit(50000);
        bad.submit(500000);
        bad.submit(2000000);

        System.out.println();
        System.out.println("========== 正解:责任链模式 ==========");
        Approver chain = new LeaderApprover("张组长");
        chain.setNext(new ManagerApprover("李经理"))
             .setNext(new DirectorApprover("王总监"))
             .setNext(new CfoApprover("赵财务总监"));

        // 客户端只认识链条的第一个节点
        chain.approve(300);
        chain.approve(8000);
        chain.approve(50000);
        chain.approve(500000);
        chain.approve(2000000);
    }
}

运行输出:

========== 反面教材:提交人自己路由 ==========
  [组长] 批准报销 300.0 元
  [经理] 批准报销 8000.0 元
  [总监] 批准报销 50000.0 元
  [财务总监] 批准报销 500000.0 元
  [无人能批] 金额过大,提交董事会讨论

========== 正解:责任链模式 ==========
  [组长张组长] 批准报销 300.0 元
  [组长张组长] 8000.0 元超出权限,转交下一级
  [经理李经理] 批准报销 8000.0 元
  [组长张组长] 50000.0 元超出权限,转交下一级
  [经理李经理] 50000.0 元超出权限,转交下一级
  [总监王总监] 批准报销 50000.0 元
  [组长张组长] 500000.0 元超出权限,转交下一级
  [经理李经理] 500000.0 元超出权限,转交下一级
  [总监王总监] 500000.0 元超出权限,转交下一级
  [财务总监赵财务总监] 批准报销 500000.0 元
  [组长张组长] 2000000.0 元超出权限,转交下一级
  [经理李经理] 2000000.0 元超出权限,转交下一级
  [总监王总监] 2000000.0 元超出权限,转交下一级
  [财务总监赵财务总监] 我是链条最后一环,无人能批,提交董事会讨论

4.3 三处细节咂摸一下

**细节一:客户端自始至终只调 chain.approve(...),只认识链头。**金额是 300 还是 200 万,客户端根本不关心——路由决策完全下放给了节点。对比反面教材里提交人背着一整套路由规则,这就是责任链的本质收益。

细节二:每个节点的三段式判断——"我的额度内 / 我有下家 / 我到头了"。amount <= 1000 是本职,next != null 是转交,else 是兜底。第三段最容易被新手漏掉:链尾没有兜底,请求就无声落空,正是第二节缺点一说的生产事故源。

细节三:setNext 返回下家引用,组链才能写成一行流式调用。chain.setNext(A).setNext(B).setNext(C)——不是炫技,是让"链条的形状"在组装代码里一目了然,一眼看出层级顺序。第五节 Netty 的 Pipeline 组装 API 就是这个味道。

4.4 纯责任链 vs 管线式责任链

本文是教科书上的纯责任链:请求被某一个节点接住就结束了,后面的节点不再参与。

但框架世界里更常见的是管线式:每个节点都处理一下(加工、校验、增强),处理完继续放行,请求穿过整条链——Servlet Filter、Spring Interceptor、Netty Pipeline 全是这种。两者的骨架代码几乎一样,区别只在节点是"拦截"还是"放行"。第五节展开看实物。

另外你可能注意到,四个具体处理者的"三段式"结构高度重复——这正是 ⑭ 模板模式的用武之地:把"判断权限→处理→转交"的骨架提到父类做成 final 方法,子类只提供阈值。责任链管"链怎么传",模板管"节点内部长什么样",两模式叠加毫不冲突。

五、在框架里找它

责任链是当之无愧的"框架骨架模式",你每天写的每个请求都在链上跑。

Servlet Filter——管线式责任链的祖师爷。

public class LogFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("请求进来:先记日志");
        chain.doFilter(req, resp);        // 放行给下一个 Filter
        System.out.println("响应回去:补一条访问记录");
    }
}

注意 FilterChain 这个参数——框架把"下家"包装成了链条对象递给节点。放行前是自己的一段前处理,放行后还能做后处理:请求沿链进、响应沿链出,一个 Filter 就是一节双向管道。编码、鉴权、日志、防 XSS……Tomcat 里的每个请求都要穿过这条链。

**Spring MVC Interceptor。**同款骨架换了个名字:preHandle 返回 true 放行、返回 false 拦停,postHandle、afterCompletion 负责回程处理。登录校验、接口限流、租户路由,实现类排成一条链。

**Netty Pipeline——把责任链做成了骨架级架构。**每个 Channel 挂一条 ChannelPipeline,编解码器、心跳处理器、业务处理器都是链上的 ChannelHandler,靠 ctx.fireChannelRead(msg) 一节节往下传(想拦截就不调用 fire)。Netty 用一条责任链撑起了整个网络事件处理模型——责任链能玩到多大,看 Netty 就知道。

**OkHttp Interceptor。**RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor、ConnectInterceptor……一次 HTTP 请求的"重试→补头→查缓存→建连接"全在链上,还开放出口让你加自定义拦截器(比了个耶:透明压缩、统一加签都是这么实现的)。Retrofit、 Glide 的原理也都有它的影子。

一句话总结这一节:凡是"请求要经过一串加工站"的地方,框架作者的第一反应都是责任链。

六、分派三部曲合订:策略、观察者、责任链

连讲三篇的"分派"问题,最后合上账本对比一下:

策略模式 ⑮ 观察者模式 ⑯ 责任链模式 ⑰
请求给谁 调用方**指定一个** **广播给全部** 沿链试探,**通常一个接盘**
决策权 调用方 名单上所有人(都执行) 每个节点自己
处理者数量 1 N(全执行) 0 或 1(纯链)/ N(管线式)
耦合方向 Context 依赖接口 Subject 依赖接口 节点只依赖下家
典型场景 促销算法、支付渠道 事件广播 审批流、过滤器

面试遇到"这三个模式有什么区别",照着这张表说,再把本文三段代码场景各举一例,基本满分。

写在最后

责任链模式一句话总结:一串处理者连成链,请求只递给链头;每个节点要么接盘,要么转交,要么兜底喊一嗓子。

下一篇 ⑱ 命令模式——责任链传的"请求"始终只是个参数,命令模式更进一步:把请求本身变成对象。编辑器的撤销键、任务队列里的一个个任务、可编排的宏录制……"把干什么打包成对象"之后,排队、撤销、日志、重放全是白送的。命令模式,下一篇见。

花

花开空白

开发者 · 西安

相关文章

评论(0)

还没有评论,来抢沙发吧

发表评论