职责链模式 ⭐
定义
责任链模式(Chain of Responsibility Pattern / 职责链模式),使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止(或者全部处理完)。
公司请假审批:你请假1天,直属组长直接批了;请假3天,组长批不了,系统自动流转给部门总监;请假10天,总监也批不了,流转给大老板。你(发送者)不需要去查到底该找谁,你只需要把请假条递给系统,它会自动顺着“责任链”往上走,直到有人能处理。
痛点:当一个请求需要经过多重条件判断和多重角色处理时,如果把所有逻辑写在一个大函数里,不仅极度臃肿,而且一旦修改其中一环,很容易波及其他环。
解决:将每一个处理节点封装成独立的对象,并在内部维持对下一个节点的引用。节点只需决定“我能处理吗?能处理就结束,不能处理就踢给下一个人”。
与设计原则的关系
- 单一职责原则 (Single Responsibility Principle):
- 责任链上的每个节点(处理者)只关心自己的处理逻辑和职责范围。比如“组长节点”只管 ≤ 3 天的判断,非常纯粹。
- 开闭原则 (Open-Closed Principle):
- 如果你想在“总监”和“大老板”之间插一个“副总裁”节点,只需要新建一个副总裁类,然后修改链条的连接顺序即可。不需要改动原来组长、总监内部的审批代码。
- 极度解耦(控制反转的思想):
- 请求发送方和接收方完全解耦。发送方甚至不知道到底是链条上的哪一个节点最终处理了请求。
实现
1. 传统 OOP 经典实现(链表式结构)
经典的责任链,各个节点通过保存 next 指针(引用)连接成一条链。
// 1. 处理者基类 (定义结构与传递逻辑)
class Approver {
constructor(name) {
this.name = name;
this.nextApprover = null; // 指向下一个节点
}
setNext(approver) {
this.nextApprover = approver;
return approver; // 方便链式调用
}
// 留给子类实现
handleRequest(days) {}
}
// 2. 具体的处理者(组长、总监、CEO)
class Manager extends Approver {
handleRequest(days) {
if (days <= 3) {
console.log(`${this.name} (组长) 批准了 ${days} 天的请假。`);
} else if (this.nextApprover) {
console.log(`${this.name} 权限不足,移交上级...`);
this.nextApprover.handleRequest(days); // 往后传!
}
}
}
class Director extends Approver {
handleRequest(days) {
if (days <= 7) {
console.log(`${this.name} (总监) 批准了 ${days} 天的请假。`);
} else if (this.nextApprover) {
console.log(`${this.name} 权限不足,移交上级...`);
this.nextApprover.handleRequest(days);
}
}
}
class CEO extends Approver {
handleRequest(days) {
console.log(`${this.name} (CEO) 最终批准了 ${days} 天的请假。`);
}
}
// 3. 组装责任链与使用
const manager = new Manager('张三');
const director = new Director('李四');
const ceo = new CEO('王老板');
// 关键步骤:把他们串联起来
manager.setNext(director).setNext(ceo);
// 客户端只管把请求丢给责任链的头部
console.log('--- 申请 2 天请假 ---');
manager.handleRequest(2);
// 张三 (组长) 批准了 2 天的请假。
console.log('\n--- 申请 5 天请假 ---');
manager.handleRequest(5);
// 张三 权限不足,移交上级...
// 李四 (总监) 批准了 5 天的请假。
console.log('\n--- 申请 15 天请假 ---');
manager.handleRequest(15);
// 张三 权限不足,移交上级...
// 李四 权限不足,移交上级...
// 王老板 (CEO) 最终批准了 15 天的请假。
2. 现代 JS 与后端的灵魂:数组变体与 next()
在现代 JavaScript(尤其是 Node.js 生态)中,我们很少用链表去实现责任链,而是用数组把函数存起来,然后提供一个 next() 方法去驱动它遍历。这通常被称为 “中间件模式 (Middleware)”。
// 极简版的中间件(责任链)调度器
class Chain {
constructor() {
this.middlewares =[]; // 用数组代替链表
this.index = 0;
}
use(fn) {
this.middlewares.push(fn);
}
run(context) {
const next = () => {
const middleware = this.middlewares[this.index++];
if (middleware) {
// 将 next 传给中间件,由中间件决定是否继续往下传
middleware(context, next);
}
};
next();
}
}
const app = new Chain();
// 第一个节点验证登录
app.use((ctx, next) => {
if (ctx.isLogin) {
console.log('登录校验通过');
next(); // 走到这才会去下一个节点
} else {
console.log('未登录,请求被拦截'); // 链条在此断开
}
});
// 第二个节点处理业务
app.use((ctx, next) => {
console.log('正在获取核心数据...', ctx.data);
next();
});
app.run({ isLogin: true, data: '订单列表' });
前端应用
如果你觉得策略模式在前端最常用,那么责任链模式可以说是现代 Web 框架的基石。
1. DOM 事件冒泡 (Event Bubbling)
这是浏览器天然的责任链模式。
当你点击了一个 <button>,浏览器会将点击事件沿着 DOM 树从内向外传递:button → div → body → document。
这正是一条责任链!如果在某个节点调用了 event.stopPropagation(),就相当于截断了责任链,不再向上抛出。
2. Axios 的拦截器 (Interceptors)
我们在前端发请求时必用的 Axios,其请求拦截器(Request)和响应拦截器(Response)底层使用的完全是责任链模式。
// 添加请求拦截器(相当于责任链的节点)
axios.interceptors.request.use(function (config) {
// 节点1:给 config 加 token
config.headers.token = 'xxx';
return config; // 传给下一个节点
});
Axios 内部将所有的拦截器放进一个大数组里,然后用一个类似 Promise 链的结构把它们依次串联执行。
3. Webpack 的 Loaders
Webpack 处理源文件时,比如要把 Less 转成最终的 CSS 注入到页面中,通常配置如下:
use:['style-loader', 'css-loader', 'less-loader']
这是一个加工型责任链:源码先进 less-loader 变成 CSS,再进 css-loader 处理 url() 等依赖,最后进 style-loader 把样式打进 DOM 中。一环扣一环,每个 loader 职责单一。
4. Express / Koa 的中间件洋葱模型
如果你接触过 Node.js 服务端开发,无论是 Express 的中间件,还是 Koa 著名的**“洋葱模型”**,本质上就是责任链模式的巅峰应用。
每一个 app.use((req, res, next) => { ... }) 都是链上的一个节点,只有调用 next() 才会将执行权移交给下一个处理者。如果某一层缓存命中了,它可以直接不调 next() 从而提前结束响应。
总结
当你遇到**“根据条件不同,去挑一个算法执行”时,请使用策略模式**(平行并列)。
当你遇到**“一个请求需要经过层层过滤、步步加工、或者寻找能最终处理它的人”时,请使用责任链模式**(串联成链)。
它把复杂的流程化逻辑切分成一个个小而美的函数/类,是编写具有高扩展性系统(如插件系统、拦截系统)的无上利器。