cv.cm

编码 · 设计模式

责任链模式详解与示例

阅读约 6 分钟

简短回答

责任链是一种行为型设计模式,它让请求沿着一条处理者对象链传递。每个处理者要么处理请求,要么把它转发给链中的下一个处理者。发送者不知道最终由哪个处理者处理请求,所以可以在不修改发送者的情况下添加、删除或重新排列处理者。Web 中间件和 DOM 事件冒泡就是众所周知的例子。

意图与它要解决的问题

意图:让多个对象都有机会处理请求,从而避免请求的发送者与接收者之间的耦合。把接收对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它。该模式是《设计模式》(四人组,GoF)一书中的行为型模式之一。

以网店的客服系统为例。金额低于 50 的退款请求,客服专员就可以批准;500 以内由组长批准;更大的金额则需要经理批准。最初的版本往往把这一切放进一个带有长长 if/else 链的函数里。每增加一条规则,这个函数就更长一些,而且检查的顺序是写死的。

有了责任链,每个级别都成为拥有一条规则的独立处理者。这些处理者被连接在一起,请求从第一个进入。增加一个新的审批级别,只需编写一个小类并把它插入链中。

结构与参与者

角色职责
处理者(Handler)声明处理请求的方法,并保存对下一个处理者的引用
具体处理者(Concrete handler)处理自己负责的请求;否则把请求转发给下一个处理者
客户端(Client)构建链(或接收一条现成的链),并把请求发给第一个处理者

常见的变体有两种。在经典形式中,第一个能处理请求的处理者会终止这条链。在管道形式(大多数中间件采用)中,每个处理者都可以先做一些工作,再决定是否调用下一个。

TypeScript 实现

一个抽象基类保存下一个处理者,并提供默认的转发行为。每个具体处理者重写 handle,在无法决定时调用 super.handle。

type Refund = { amount: number };
abstract class Approver {
  private next?: Approver;
  setNext(next: Approver): Approver {
    this.next = next;
    return next; // allows a.setNext(b).setNext(c)
  }
  handle(req: Refund): string {
    return this.next ? this.next.handle(req) : "rejected: no one can approve " + req.amount;
  }
}
class Agent extends Approver {
  handle(req: Refund): string {
    return req.amount < 50 ? "agent approved " + req.amount : super.handle(req);
  }
}
class TeamLead extends Approver {
  handle(req: Refund): string {
    return req.amount <= 500 ? "team lead approved " + req.amount : super.handle(req);
  }
}
class Manager extends Approver {
  handle(req: Refund): string {
    return req.amount <= 5000 ? "manager approved " + req.amount : super.handle(req);
  }
}
const chain = new Agent();
chain.setNext(new TeamLead()).setNext(new Manager());
for (const amount of [20, 300, 2000, 9000]) console.log(chain.handle({ amount }));
// agent approved 20
// team lead approved 300
// manager approved 2000
// rejected: no one can approve 9000

用普通函数实现的中间件形式

在 TypeScript 中并不需要类。一个函数列表,其中每个函数接收一个 next 回调,就构成了一条链。Express 和 Koa 的中间件就是这样工作的。

type Req = { user?: string; path: string };
type Middleware = (req: Req, next: () => string) => string;
const auth: Middleware = (req, next) => (req.user ? next() : "401 Unauthorized");
const log: Middleware = (req, next) => {
  console.log("request", req.path);
  return next();
};
const route = (req: Req) => "200 OK " + req.path;
function run(chain: Middleware[], req: Req): string {
  const step = (i: number): string =>
    i < chain.length ? chain[i](req, () => step(i + 1)) : route(req);
  return step(0);
}
console.log(run([log, auth], { path: "/admin" }));
console.log(run([log, auth], { user: "ana", path: "/admin" }));
// request /admin
// 401 Unauthorized
// request /admin
// 200 OK /admin

何时使用,何时不用

  • 当多个对象都可能处理请求,并且由运行时决定哪一个合适时,使用它。
  • 当处理者的集合或顺序需要可配置时使用,例如校验步骤、审批级别或 HTTP 中间件。
  • 当你希望每条规则都放在各自的小单元中,便于单独测试时使用。
  • 对于两三个永远不变的固定检查,不要使用它;简单的 if 语句更清晰。
  • 要意识到,过长的链更难调试,因为从调用处看不出最后是哪个处理者起了作用。在处理者内部记录日志会有帮助。
  • 链对顺序很敏感。例如,把身份验证放在路由处理者之后,就违背了它的初衷。

实际应用

  • Express 和 Koa 中间件:每个函数接收请求和一个 next 函数,并决定是否继续。
  • DOM 事件传播:事件从目标元素向上经过它的各级祖先冒泡,任何监听器都可以用 event.stopPropagation() 阻止它。
  • Java Servlet 过滤器构成过滤器链,其中每个过滤器调用 chain.doFilter 把请求传递下去。
  • 日志框架会把日志记录从一个 logger 传递给它的父 logger 及其处理器。

相关模式

模式与责任链的区别
装饰装饰也形成一条包装链,但每个装饰器总是会运行并转发;处理者则可以终止这条链。
命令链中的请求往往就是一个命令对象;命令关注的是如何封装请求,而责任链关注的是找出谁来处理它。
组合组合树中的父节点天然就是下一个处理者,GUI 事件就是这样沿着组件树向上冒泡的。
中介者中介者通过一个了解所有接收者的中心对象来路由消息;责任链则是把请求从一个处理者传给下一个。

常见问题

链中的每个处理者都必须处理请求吗?

不必。在经典形式中,第一个能处理请求的处理者会终止这条链。在中间件形式中,每个处理者可以做一些工作,然后调用下一个,因此多个处理者可以作用于同一个请求。

Express 中间件是责任链的例子吗?

是的。每个中间件函数接收请求和一个 next 回调,它要么结束响应,要么调用 next 把控制权交给下一个中间件。

如果没有处理者处理请求会怎样?

这取决于你的设计。好的做法是在链的末端添加一个默认处理,比如返回错误、抛出异常或记录日志,这样请求就不会悄悄丢失。

它与一串 if/else 语句有什么不同?

逻辑是一样的,但每个条件都变成了单独的对象或函数。这使得链可以在运行时配置,并且无需编辑一个庞大的函数就能添加步骤或调整顺序。

全部编码教程