设计模式修炼之路 ⑰ 责任链模式——请求沿链条传递
客户端只认识链头、节点自治三段式、纯链与管线式、从 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 万?签不动,转给经理。你从不需要知道整条审批线长什么样,你只认识你组长。
和前两篇串起来看,行为型已经凑齐了"分派三部曲":⑮ 策略解决"选哪个算法",⑯ 观察者解决"通知哪些下游",⑰ 责任链解决"请求谁接盘"——前两个的决策权在调用方/名单,这一个的决策权下放到每个节点自己手里。
二、优点与缺点
优点:
- 发送者只认识链头。客户端不知道有几个处理者、怎么路由,耦合度降到最低。
- 节点自治。每个处理者只关心"我的额度是多少、批还是转",节点之间互不认识——除了自己的下家。
- 动态组链。链条是运行时拼的:加一级审批、临时抽掉一个环节、换个顺序,都是改一行组装代码的事,节点类不动。
- 符合开闭原则。新加一个 VP 审批层 = 写一个类 + 组链时插进去。
缺点:
- 请求可能没人处理。链走到头还是没人接,请求就"落空"了——本例 200 万报销单就走到了链尾。必须设计好兜底(驳回、默认处理、或者日志告警),否则请求无声消失,是生产事故级 bug。
- 调试费劲。一个请求在链上跳来跳去,出问题时要在每个节点打断点、看日志,不如一个 if-else 直观。
- 性能损耗。权限明明在总监手里,请求还是要从组长、经理白跑两站。链越长、命中越靠后,浪费越多。
- 顺序敏感。组链的人把总监排在组长前面,整条链的逻辑就乱了。链条的正确性靠组装代码保证,编译器帮不了你。
三、什么时候用
判断标准:同一个请求,有多个候选处理者,该谁处理要由"请求内容 + 处理者能力"在运行时决定,且发送者不该知道路由规则。
- 层级审批:报销、请假、合同会签——本文场景
- 规则拦截:风控系统里"黑名单→限频→额度"层层筛查,谁先拦住谁处理
- 请求预处理管线:编码、鉴权、日志、防攻击……每个环节处理自己的部分再放行(见第五节 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 依赖接口 | 节点只依赖下家 |
| 典型场景 | 促销算法、支付渠道 | 事件广播 | 审批流、过滤器 |
面试遇到"这三个模式有什么区别",照着这张表说,再把本文三段代码场景各举一例,基本满分。
写在最后
责任链模式一句话总结:一串处理者连成链,请求只递给链头;每个节点要么接盘,要么转交,要么兜底喊一嗓子。
下一篇 ⑱ 命令模式——责任链传的"请求"始终只是个参数,命令模式更进一步:把请求本身变成对象。编辑器的撤销键、任务队列里的一个个任务、可编排的宏录制……"把干什么打包成对象"之后,排队、撤销、日志、重放全是白送的。命令模式,下一篇见。