Appearance
5.5 责任链模式 (Chain of Responsibility)
欢迎来到行为型模式,也是我们整个设计模式手册的最后一站!我们将学习一个旨在构建松耦合、可动态组合的请求处理流程的模式——责任链模式。
核心定义:为了避免请求发送者与多个接收者耦合在一起,将所有接收者串成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
一、生活中的场景:公司报销审批流程
这个模式最经典的比喻就是公司的报销审批流程:
- 你 (Client):提交了一张 8000 元的报销单(Request)。
- 小组长 (Handler 1):你先把单子交给小组长。他一看金额,超过了他的审批权限(比如 500 元)。他不会打回你的申请,而是签上字,然后传递给他的上级。
- 部门经理 (Handler 2):经理看到申请,8000 元也超过了他的权限(比如 5000 元)。于是,他继续传递给财务总监。
- 财务总监 (Handler 3):总监一看,8000 元在他的权限范围内(比如 10000 元)。于是,他批准了申请,处理完成。这个请求的传递过程到此结束。
在这个流程中:
- 请求在链上传递:报销单沿着“小组长 -> 经理 -> 总监”这条链流动。
- 发送者与接收者解耦:你作为申请人,根本不需要知道你的申请最终会由谁来批准。你只需要把申请交给链的第一个人(小组长)即可。
- 动态组合:如果公司流程变更,可以在经理和总监之间再增加一个“事业部总监”的审批节点,对你来说是完全无感的。
二、没有责任链模式的代码:臃肿的验证函数
假设我们要做一个用户注册的表单验证。我们需要校验用户名和密码。
javascript
function validate(username, password) {
// 校验 1:用户名不能为空
if (username === "") {
console.error("用户名不能为空!");
return false;
}
// 校验 2:密码长度不能少于 8 位
if (password.length < 8) {
console.error("密码长度不能少于 8 位!");
return false;
}
// 校验 3:用户名不能包含特殊字符
if (/[^a-zA-Z0-9]/.test(username)) {
console.error("用户名只能包含字母和数字!");
return false;
}
// ... 未来可能还有更多的校验规则
return true;
}
validate("testuser", "1234"); // 密码长度不能少于 8 位!这种“面条式”的代码问题很明显:
- 高耦合:所有的校验逻辑都耦合在一个函数里。
- 违反开放/封闭原则:每当需要新增或修改一个校验规则,都必须深入这个函数内部进行修改。
- 复用性差:如果另一个表单也需要“非空校验”,只能复制粘贴代码。
三、使用责任链模式重构 (函数式实现)
在 JavaScript 中,使用函数作为链上的节点,可以非常优雅地实现责任链模式。这在 Express/Koa 等 Node.js 框架的中间件 (Middleware) 机制中被发扬光大。
我们的目标是:把每一个 if 判断都封装成一个独立的“处理器”函数。
第 1 步:定义处理器 (Handlers)
每个处理器都是一个函数,它接收一个 next 函数作为参数,用于将请求传递给链上的下一个节点。
javascript
function validateUsernameEmpty(username, password, next) {
if (username === "") {
return console.error("用户名不能为空!"); // 处理请求,中断链条
}
next(); // 传递给下一个
}
function validatePasswordLength(username, password, next) {
if (password.length < 8) {
return console.error("密码长度不能少于 8 位!");
}
next();
}
function validateUsernameChars(username, password, next) {
if (/[^a-zA-Z0-9]/.test(username)) {
return console.error("用户名只能包含字母和数字!");
}
next();
}第 2 步:创建链条 (Chain)
我们需要一个辅助类来管理和启动这条链。
javascript
class Chain {
constructor() {
this.handlers = [];
}
use(handler) {
this.handlers.push(handler);
return this;
}
start(...args) {
let index = 0;
const next = () => {
if (index < this.handlers.length) {
const handler = this.handlers[index++];
handler(...args, next);
} else {
// 所有校验通过
console.log("所有校验通过!");
}
};
next();
}
}第 3 步:客户端使用
现在,客户端可以像搭积木一样,自由地组织这条验证链。
javascript
const validationChain = new Chain();
// 声明式地构建链条
validationChain
.use(validateUsernameEmpty)
.use(validatePasswordLength)
.use(validateUsernameChars);
console.log("--- 案例 1 ---");
validationChain.start("testuser", "1234"); // 密码长度不能少于 8 位!
console.log("\n--- 案例 2 ---");
validationChain.start("", "12345678"); // 用户名不能为空!
console.log("\n--- 案例 3 ---");
validationChain.start("testuser", "12345678"); // 所有校验通过!看,我们的代码变得多么清晰、可维护和可扩展!如果需要新增一个“密码必须包含大写字母”的校验,我们只需要:
- 新建一个
validatePasswordCapital函数。 - 在链中
.use(validatePasswordCapital)一下即可。
四、优缺点与适用场景
优点
- 高度解耦:请求的发送者完全不需要知道请求被哪个对象处理了,也不知道链的结构。
- 灵活性高:可以动态地、在运行时修改或组合链的结构。
- 符合开放/封闭原则:可以很容易地通过增加新的处理器来扩展功能,而无需修改已有代码。
- 职责清晰:每个处理器都只关心自己的职责,符合单一职责原则。
缺点
- 性能问题:请求需要遍历链上的多个节点,对于长链条,可能会有性能损耗。
- 请求不一定被处理:请求可能到达链的末端,仍然没有被任何处理器处理。
- 调试困难:由于请求在链中跳转,可能会增加调试的复杂度。
适用场景
- 审批/工作流:如报销、请假、发布流程等。
- Web 框架的中间件:Express, Koa 的中间件机制是责任链模式的典范应用,用于处理日志、认证、CORS、数据解析等。
- JavaScript 的事件冒泡机制:一个事件从 DOM 树的深层节点开始,逐级向上传递,直到被某个节点的事件监听器处理或到达根节点。
- 多级缓存查询:一个查询请求先经过浏览器缓存、再到 CDN 缓存、再到服务器缓存、最后到数据库。
全书总结
恭喜你!至此,我们已经完成了对所有三大类核心设计模式的探索。
- 创建型模式 教会我们如何优雅地创建对象。
- 结构型模式 教会我们如何灵活地组装对象。
- 行为型模式 教会我们如何高效地组织对象间的协作。
设计模式不是银弹,更不是需要死记硬背的教条。它们是前人智慧的结晶,是应对特定问题的“武功秘籍”。真正掌握它们的关键在于理解每个模式背后的“意图”——它究竟是为了解决什么样的问题而存在的。
希望这本手册能成为你代码修行路上的良师益友。在未来的开发实践中,当你再次遇到棘手的、似曾相识的问题时,愿你能回想起这里的某个“锦囊”,写出更优雅、更健壮、更具艺术感的代码。
Happy Coding!