跳转到内容

第 23 章 · 责任链(Chain of Responsibility)

一句话:把几个处理者连成一条链,请求从链头进去,每个处理者要么处理掉,要么交给下一个。 解决的痛:谁来处理写成一串 if,加一道检查要找准插在第几行;几个渠道各复制一份,漏改一份不报错,直到有人钻了空子。

你在电商公司做售后系统。用户申请退款后,系统要决定这笔退款由谁来批:

  • 100 元以下,客服直接批,系统马上放款;
  • 100 元到 1000 元以下,主管批;
  • 1000 元及以上,财务批。

退款申请从两个地方进来:App 和微信小程序。两边调的是同一个函数,传进一笔退款申请,返回“谁批准了”。本章只算审批结果,不连数据库,金额以元为单位,申请编号 R1、R2 这样排。

金额分三档,从小往大判断,命中哪档就返回哪档:

from dataclasses import dataclass
@dataclass
class RefundRequest:
req_id: str
amount: int # 退款金额(元)
def approve_refund(req: RefundRequest) -> str:
if req.amount < 100:
return "客服批准"
if req.amount < 1000:
return "主管批准"
return "财务批准"
if __name__ == "__main__":
requests = [RefundRequest("R1", 600), RefundRequest("R2", 80), RefundRequest("R3", 3000)]
for req in requests:
print(f"{req.req_id} {req.amount} 元:{approve_refund(req)}")
# 输出:
# R1 600 元:主管批准
# R2 80 元:客服批准
# R3 3000 元:财务批准

TS 版结构一样:

interface RefundRequest {
reqId: string;
amount: number; // 退款金额(元)
}
function approveRefund(req: RefundRequest): string {
if (req.amount < 100) return "客服批准";
if (req.amount < 1000) return "主管批准";
return "财务批准";
}
const requests: RefundRequest[] = [
{ reqId: "R1", amount: 600 },
{ reqId: "R2", amount: 80 },
{ reqId: "R3", amount: 3000 },
];
for (const req of requests) {
console.log(`${req.reqId} ${req.amount} 元:${approveRefund(req)}`);
}
// 输出:
// R1 600 元:主管批准
// R2 80 元:客服批准
// R3 3000 元:财务批准

三档规则一眼看完,判断顺序就是代码顺序。规则只有这几条、两个渠道也一样的时候,这样写清楚直接,大多数人第一版都这么写。

运营提了三件事:一是最前面加一道风控,同一个用户 24 小时内第三次申请退款,不管金额多少都转人工审核;二是 VIP 用户 500 元以下的退款由客服直接批;三是两个渠道的流程分开,小程序没有会员体系,不设 VIP 通道。

小王要改三处:

  1. 风控:RefundRequest 加字段 refunds_24h(这个用户 24 小时内已经退过几次,调用方查好填进来);函数第一行插一个判断,必须在所有金额判断之前,否则小额退款先被客服批掉,风控根本轮不到。
  2. VIP 通道:加字段 vip;插一个 if req.vip and req.amount < 500,必须放在“小于 1000”那行之前。原来“按金额从小到大”的顺序被打乱了,判断条件开始交织:300 元的 VIP 申请同时满足两条,哪条在前由行号决定。
  3. 分渠道:函数复制成两份,approve_refund_app 和 approve_refund_mini,App 和小程序的入口各调各的。以后改审批规则,两份都要改。

小王先把原来的函数复制一份给小程序(小程序不要 VIP 通道,这份就没再改);再去 App 那份加风控和 VIP。小程序那份就停在了复制时的样子。改完是这样:

@dataclass
class RefundRequest:
req_id: str
amount: int # 退款金额(元)
vip: bool = False # ②
refunds_24h: int = 0 # ① 这个用户 24 小时内已经退过几次,调用方查好填进来
def approve_refund_app(req: RefundRequest) -> str: # ③ App 用这份
if req.refunds_24h >= 2: # ① 第三次退款转人工,必须在最前面
return "风控转人工"
if req.vip and req.amount < 500: # ② 必须在"小于 1000"之前
return "客服批准(VIP)"
if req.amount < 100:
return "客服批准"
if req.amount < 1000:
return "主管批准"
return "财务批准"
def approve_refund_mini(req: RefundRequest) -> str: # ③ 小程序用这份,复制自原来的函数
if req.amount < 100:
return "客服批准"
if req.amount < 1000:
return "主管批准"
return "财务批准"

把这几处换进 before.py、删掉原来的 approve_refund,入口换成下面这段。R3 是上线后出事的那种申请:同一个用户 24 小时内的第三次退款,80 元:

if __name__ == "__main__":
requests = [
RefundRequest("R1", 600),
RefundRequest("R2", 300, vip=True),
RefundRequest("R3", 80, refunds_24h=2), # 24 小时内第三次退款
]
for req in requests:
print(f"{req.req_id} {req.amount} 元:App {approve_refund_app(req)},小程序 {approve_refund_mini(req)}")

运行结果如下(TS 版改的是同样的地方,输出相同):

R1 600 元:App 主管批准,小程序 主管批准
R2 300 元:App 客服批准(VIP),小程序 主管批准
R3 80 元:App 风控转人工,小程序 客服批准

前两行都对:小程序没有 VIP 通道,300 元该主管批。第三行出了事:同样的第三次退款,App 拦住了,小程序由客服直接批了,钱马上放出去。程序没有报错,测试也只测了 App 那份。等风控同事发现有人专挑小程序、每天退好几笔小额,已经过去两周。

问题的形状是:每加一道审批环节,就要在每份函数里找准它该插的那一行;渠道有几个,同一条规则就要抄几遍,漏抄一份不报错。

直觉写法:两个渠道各一份 if 链,小程序那份没有风控

图中 ①②③ 就是上面三处:黄色是 App 那份新插的两个判断,浅红是加了字段的 RefundRequest,红色加粗的框是那次事故:小程序那份从第一行就开始判断金额,第三次退款落进了“小于 100”。

换个思路:每一档审批写成一个处理者(handler),它只回答一件事:“这笔申请归不归我管”。归它管,它给出决定;不归它管,交给下一个。几个处理者按顺序连起来,申请从第一个进去,走到有人处理为止。

基类 Handler 记住下一个处理者是谁,提供 set_next 和 pass_on;每个具体处理者实现 handle,返回一个决定 Decision,没人处理时返回 None。每个渠道的链在一个地方拼好:

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class RefundRequest:
req_id: str
amount: int # 退款金额(元)
vip: bool = False
refunds_24h: int = 0 # 这个用户 24 小时内已经退过几次,调用方查好填进来
@dataclass(frozen=True)
class Decision:
by: str # 谁处理的
verdict: str # 批准、转人工
def __str__(self) -> str:
return f"{self.by}{self.verdict}"
class Handler(ABC):
def __init__(self) -> None:
self._next: Handler | None = None
def set_next(self, nxt: Handler) -> None:
self._next = nxt
def pass_on(self, req: RefundRequest) -> Decision | None: # 不归我管:交给下一个
return self._next.handle(req) if self._next else None
@abstractmethod
def handle(self, req: RefundRequest) -> Decision | None: ...
class RiskCheck(Handler):
def handle(self, req: RefundRequest) -> Decision | None:
if req.refunds_24h >= 2: # 24 小时内第三次
return Decision("风控", "转人工")
return self.pass_on(req)
class VipFastLane(Handler):
def handle(self, req: RefundRequest) -> Decision | None:
if req.vip and req.amount < 500:
return Decision("客服", "批准(VIP)")
return self.pass_on(req)
class CustomerService(Handler):
def handle(self, req: RefundRequest) -> Decision | None:
if req.amount < 100:
return Decision("客服", "批准")
return self.pass_on(req)
class Supervisor(Handler):
def handle(self, req: RefundRequest) -> Decision | None:
if req.amount < 1000:
return Decision("主管", "批准")
return self.pass_on(req)
class Finance(Handler):
def handle(self, req: RefundRequest) -> Decision | None:
return Decision("财务", "批准") # 链尾:什么金额都接
def build_chain(*handlers: Handler) -> Handler:
for current, nxt in zip(handlers, handlers[1:]):
current.set_next(nxt)
return handlers[0]
def make_chains() -> dict[str, Handler]: # 登记处:每个渠道的链在这里拼,顺序就是审批顺序
return {
"App": build_chain(RiskCheck(), VipFastLane(), CustomerService(), Supervisor(), Finance()),
"小程序": build_chain(RiskCheck(), CustomerService(), Supervisor(), Finance()),
}
if __name__ == "__main__":
chains = make_chains()
requests = [
RefundRequest("R1", 600),
RefundRequest("R2", 300, vip=True),
RefundRequest("R3", 80, refunds_24h=2),
]
for req in requests:
app, mini = chains["App"].handle(req), chains["小程序"].handle(req)
print(f"{req.req_id} {req.amount} 元:App {app},小程序 {mini}")
# 输出:
# R1 600 元:App 主管批准,小程序 主管批准
# R2 300 元:App 客服批准(VIP),小程序 主管批准
# R3 80 元:App 风控转人工,小程序 风控转人工

TS 版结构相同:

interface RefundRequest {
reqId: string;
amount: number; // 退款金额(元)
vip?: boolean;
refunds24h?: number; // 这个用户 24 小时内已经退过几次,调用方查好填进来
}
class Decision {
constructor(readonly by: string, readonly verdict: string) {} // by:谁处理的;verdict:批准、转人工
toString(): string {
return `${this.by}${this.verdict}`;
}
}
abstract class Handler {
private next: Handler | null = null;
setNext(next: Handler): void {
this.next = next;
}
protected passOn(req: RefundRequest): Decision | null { // 不归我管:交给下一个
return this.next ? this.next.handle(req) : null;
}
abstract handle(req: RefundRequest): Decision | null;
}
class RiskCheck extends Handler {
handle(req: RefundRequest): Decision | null {
if ((req.refunds24h ?? 0) >= 2) return new Decision("风控", "转人工"); // 24 小时内第三次
return this.passOn(req);
}
}
class VipFastLane extends Handler {
handle(req: RefundRequest): Decision | null {
if (req.vip && req.amount < 500) return new Decision("客服", "批准(VIP)");
return this.passOn(req);
}
}
class CustomerService extends Handler {
handle(req: RefundRequest): Decision | null {
if (req.amount < 100) return new Decision("客服", "批准");
return this.passOn(req);
}
}
class Supervisor extends Handler {
handle(req: RefundRequest): Decision | null {
if (req.amount < 1000) return new Decision("主管", "批准");
return this.passOn(req);
}
}
class Finance extends Handler {
handle(_req: RefundRequest): Decision | null {
return new Decision("财务", "批准"); // 链尾:什么金额都接
}
}
function buildChain(...handlers: Handler[]): Handler {
for (let i = 0; i + 1 < handlers.length; i++) handlers[i].setNext(handlers[i + 1]);
return handlers[0];
}
function makeChains(): Record<string, Handler> { // 登记处:每个渠道的链在这里拼,顺序就是审批顺序
return {
App: buildChain(new RiskCheck(), new VipFastLane(), new CustomerService(), new Supervisor(), new Finance()),
小程序: buildChain(new RiskCheck(), new CustomerService(), new Supervisor(), new Finance()),
};
}
const chains = makeChains();
const requests: RefundRequest[] = [
{ reqId: "R1", amount: 600 },
{ reqId: "R2", amount: 300, vip: true },
{ reqId: "R3", amount: 80, refunds24h: 2 },
];
for (const req of requests) {
const app = chains["App"].handle(req);
const mini = chains["小程序"].handle(req);
console.log(`${req.reqId} ${req.amount} 元:App ${app},小程序 ${mini}`);
}
// 输出:
// R1 600 元:App 主管批准,小程序 主管批准
// R2 300 元:App 客服批准(VIP),小程序 主管批准
// R3 80 元:App 风控转人工,小程序 风控转人工

责任链版:五个处理者实现同一个 Handler,两个渠道各拼一条链

看黄色的两个处理者:风控和 VIP 通道是这次新加的,各是一个类。CustomerService、Supervisor、Finance 就是原来的三档金额判断,各搬进一个类,条件一个没变。下半部分是 make_chains 拼出的两条链,小程序那条少了 VipFastLane,但两条都从 RiskCheck 开头。

一笔 600 元的申请在 App 链上是这样走的:

一次审批:600 元的申请依次经过风控、VIP 通道、客服,在主管处被批准

每个处理者只看自己那一条条件,不满足就调 pass_on;Supervisor 给出决定后直接返回,Finance 根本没被调用,决定沿原路一层层返回给入口。

逐条对照 23.3:

  • ① 风控:写在 RiskCheck 里,两个渠道的链都把它放在第一个。不用在一串 if 里找“该插在第几行”,它在链上的位置就是 build_chain 参数里的位置。
  • ② VIP 通道:写在 VipFastLane 里,它只管“VIP 且不到 500”,不用关心后面还有几档。
  • ③ 分渠道:不再复制函数,处理者两个渠道共用,每个渠道只是一行 build_chain(...)。改某一档的规则,比如客服额度提到 150 元,只改 CustomerService 一处,两条链都生效。
  • 事故:小程序的链也以 RiskCheck 开头,R3 转了人工。
  • 再加一个渠道,比如网页版:在 make_chains 里多拼一条链;再加一道环节,比如“大促期间 200 元以下自动批”:写一个处理者类,插进需要它的那几条链。

有几件事要说清楚:

  • 顺序还是要人来定。链的顺序现在写在 make_chains 里,写错了照样出事:假如有人把小程序那条写成 build_chain(CustomerService(), RiskCheck(), …),80 元的第三次退款又会先被客服批掉,程序同样不报错。区别是,错也只错在一个地方:每个渠道一行,评审时把几行并排看,谁排第一、谁缺了一目了然,不用在两份函数里逐行比对。
  • 处理者对象记着“下一个是谁”,所以同一个对象不能同时放进两条链。两条链要各 new 一套,make_chains 就是这样做的;要是两条链共用一个 Supervisor 对象,后拼的那条会把它的 _next 改掉。
  • 类变多了,结果要顺着链才看得出。三行 if 变成五个类加一个拼链函数;想知道 600 元的申请谁批,要先找到对应渠道的链,再一个个看过去。23.7 会说链长了以后怎么办。

责任链换来的是每一档审批是一个独立的类,链的顺序写在拼链的地方,每个渠道一行。代价是类多了,顺序的正确性仍然靠人保证,只是集中到了一处。

GoF 对责任链的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

“避免发送者和接收者之间的耦合”,落到本章就是:App 的退款入口只调 chains["App"].handle(req),它不知道、也不需要知道这笔申请最后是客服、主管还是财务批的。哪档规则变了、加了风控,入口代码一行不改;23.3 里每改一次规则,两个入口调的函数都得跟着改。

责任链的结构:Client 只把请求交给链头,每个 Handler 持有下一个

看 Handler 指向自己的那条线:每个处理者持有一个后继(successor),类型就是 Handler 本身,所以链可以任意长,Client 也只认识链头。

角色 在本章案例里是谁 职责
处理者(Handler) Handler 声明处理请求的方法 handle,持有下一个处理者,提供“交给下一个”的 pass_on
具体处理者(ConcreteHandler) RiskCheck、VipFastLane、CustomerService、Supervisor、Finance 判断请求归不归自己管,归就处理,不归就交给下一个
客户(Client) App 和小程序的退款入口 把请求交给链头,不关心谁处理

本章这种叫纯粹的责任链:一个请求最多只有一个处理者真正处理它,谁处理了,链就停在谁那里。实际系统里更多见的是另一种用法,常叫管道(pipeline):每个处理者都对请求做一点事,记日志、加请求头、计时,然后交给下一个,一直走到底。Web 框架的中间件(middleware)更接近管道,只是每一层也可以中途截断,比如鉴权失败直接返回,后面的都不执行,23.6 的两个例子都是这样。两种都叫责任链,读代码时先看清楚是哪种:纯粹的链只关心“谁处理了”,管道还要关心“每一层都做了什么”。

常见场景:

  • 审批流。按金额、类型、部门逐级判断,本章就是。
  • Web 中间件。日志、鉴权、限流、压缩一层层排开,请求依次穿过,见 23.6。
  • 异常处理。Python、TS 里抛出的异常沿调用栈往外传,遇到第一个匹配的 except 或 catch 就停下,没人接就一直传到最外层,程序报错退出。
  • 界面的事件冒泡(event bubbling)。浏览器里点击一个按钮,click 事件先交给按钮,再交给外面的 div、再往外,任何一层的监听函数调 event.stopPropagation() 就不再往外传。

Django 的每个中间件是一个工厂:启动时 Django 调它一次,传入 get_response,也就是“下一层”,最里面那层是视图;它返回一个可调用对象,之后每个请求都调这个对象。中间件可以在调 get_response 之前处理请求、之后处理响应,也可以不调它,直接返回一个响应,后面的中间件和视图就都不执行了。settings.MIDDLEWARE 里排在前面的在外层,Django 加载时从列表末尾往前一层层包,大致如此。下面模拟三个中间件:日志、鉴权(未登录直接返回 403)、计时:

from collections.abc import Callable
class HttpRequest: # 模拟 django.http.HttpRequest,只留用得到的字段
def __init__(self, path: str, user: str | None) -> None:
self.path = path
self.user = user
class HttpResponse: # 模拟 django.http.HttpResponse
def __init__(self, content: str, status: int = 200) -> None:
self.content = content
self.status_code = status
self.headers: dict[str, str] = {}
GetResponse = Callable[[HttpRequest], HttpResponse]
clock = [0] # 假时钟(毫秒),让输出固定;真实代码用 time.monotonic()
def logging_middleware(get_response: GetResponse) -> GetResponse:
def middleware(request: HttpRequest) -> HttpResponse:
print(f"日志:→ {request.path}")
response = get_response(request) # 交给下一层
print(f"日志:← {response.status_code}")
return response
return middleware
def auth_middleware(get_response: GetResponse) -> GetResponse:
def middleware(request: HttpRequest) -> HttpResponse:
if request.user is None: # 短路:不调 get_response,计时和视图都不执行
return HttpResponse("请先登录", status=403)
return get_response(request)
return middleware
def timing_middleware(get_response: GetResponse) -> GetResponse:
def middleware(request: HttpRequest) -> HttpResponse:
start = clock[0]
response = get_response(request)
response.headers["X-Elapsed-Ms"] = str(clock[0] - start)
return response
return middleware
def refund_list_view(request: HttpRequest) -> HttpResponse:
clock[0] += 30 # 假装视图花了 30 毫秒
return HttpResponse(f"{request.user} 的退款记录")
MIDDLEWARE = [logging_middleware, auth_middleware, timing_middleware] # 对应 settings.MIDDLEWARE
def load_middleware(view: GetResponse) -> GetResponse: # 模拟 BaseHandler.load_middleware:从后往前包
handler = view
for factory in reversed(MIDDLEWARE):
handler = factory(handler)
return handler
if __name__ == "__main__":
handler = load_middleware(refund_list_view)
for req in [HttpRequest("/refunds", "alice"), HttpRequest("/refunds", None)]:
resp = handler(req)
print(f"{resp.status_code} {resp.content} {resp.headers}")
# 输出:
# 日志:→ /refunds
# 日志:← 200
# 200 alice 的退款记录 {'X-Elapsed-Ms': '30'}
# 日志:→ /refunds
# 日志:← 403
# 403 请先登录 {}

三个中间件是处理者,get_response 就是本章的 _next,调它就是 pass_on;load_middleware 是拼链的地方,对应 make_chains。和本章案例不同的是,日志和计时每次都往下传、也都做了事,这是管道;只有鉴权会截断,未登录的请求连计时都没进,响应头里也就没有耗时。

例二:Express 的 app.use(TypeScript)

Section titled “例二:Express 的 app.use(TypeScript)”

Node.js 的 Web 框架 Express 用 app.use((req, res, next) => …) 注册中间件,按注册顺序排成一串。中间件做完自己的事,调 next() 交给下一个;不调 next(),比如已经用 res.send() 回了响应,链就在这里结束。所有中间件都没回响应,Express 最后回一个 404,正文类似 Cannot GET /orders,大致如此。下面模拟一个最小的 use 和 handle:

interface Req { // 模拟 Express 的 req,只留用得到的字段
method: string;
path: string;
headers: Record<string, string>;
user?: string;
}
class Res { // 模拟 Express 的 res
statusCode = 200;
sent = false;
status(code: number): this {
this.statusCode = code;
return this;
}
send(body: string): void {
this.sent = true;
console.log(`→ ${this.statusCode} ${body}`);
}
}
type Middleware = (req: Req, res: Res, next: () => void) => void;
class MiniApp { // 模拟 express() 返回的 app;真实的分发写在 Router 里,大致如此
private stack: Middleware[] = [];
use(fn: Middleware): void {
this.stack.push(fn);
}
handle(req: Req, res: Res): void {
let i = 0;
const next = (): void => {
const fn = this.stack[i++];
if (fn) fn(req, res, next);
else if (!res.sent) res.status(404).send(`Cannot ${req.method} ${req.path}`); // 走到底没人回
};
next();
}
}
const app = new MiniApp();
app.use((req, _res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
app.use((req, res, next) => {
const auth = req.headers["authorization"];
if (!auth) return res.status(401).send("请先登录"); // 不调 next():链在这里结束
req.user = auth.replace("Bearer ", "");
next();
});
app.use((req, res, next) => {
if (req.path === "/refunds") res.send(`${req.user} 的退款记录`);
else next();
});
const bearer = { authorization: "Bearer alice" };
app.handle({ method: "GET", path: "/refunds", headers: bearer }, new Res());
app.handle({ method: "GET", path: "/refunds", headers: {} }, new Res());
app.handle({ method: "GET", path: "/orders", headers: bearer }, new Res());
// 输出:
// GET /refunds
// → 200 alice 的退款记录
// GET /refunds
// → 401 请先登录
// GET /orders
// → 404 Cannot GET /orders

每个中间件是一个处理者,next 就是 pass_on;第三个中间件只管 /refunds,别的路径交出去,和本章的 CustomerService 只管 100 元以下是一个意思。和 Django 不同,Express 的处理者不持有“下一个”,next 是 handle 每次现做的闭包,靠下标 i 记走到了哪,所以同一个中间件函数可以注册进好几个 app。

  • 处理者固定,只有一两个。比如“管理员直接通过,其他人走审核”,一个 if 就说完了。拆成两个处理者类加一个拼链函数,读代码的人要翻三处才知道就这一个判断,而且多半永远不会有第三档。
  • 链很长,又没人记录走到了哪。链有七八个处理者时,请求可能“消失”在链里:某个新加的处理者忘了调 pass_on,handle 返回了 None;或者链尾没有兜底的 Finance,大额申请走完一圈没人接。程序不报错,App 上这笔退款一直显示“处理中”;用户三天后投诉,排查的人只能把每个处理者读一遍,猜是谁吞了它。办法是让每个处理者记下自己处理了没有,没人处理时报警,而不是默默返回。给 RefundRequest 加一个 trail: list[str] 字段,再把“交给下一个”收回基类,子类只回答处不处理(这就是第 20 章的模板方法):
class Handler(ABC):
def handle(self, req: RefundRequest) -> Decision | None:
decision = self.check(req) # 子类只实现 check:归我管就返回决定,否则返回 None
req.trail.append(f"{type(self).__name__}:{decision or '跳过'}")
if decision is not None or self._next is None:
return decision
return self._next.handle(req)

入口拿到 None 时把 req.trail 打进日志并报警,一眼就能看到“走到哪一个为止、每一个都说了什么”。子类也不再有“忘了往下传”的机会。

  • 处理顺序有隐含依赖,却没人写明。本章的风控必须排第一,VIP 通道必须在主管之前,这两条依赖只存在于 make_chains 的参数顺序里。新同事加渠道时照着别的链抄,顺手把 RiskCheck 挪到后面“让常见情况先走”,23.4 说过的事故就回来了。处理者之间一旦有这种依赖,要么在拼链处写清注释并加一条测试(每条链的第一个必须是 RiskCheck),要么干脆不让各处自己拼,由一个函数统一生成链,只让调用方选择要不要 VIP 通道。
模式 意图 区别
责任链(Chain of Responsibility) 让请求沿一条链传递,找到处理它的对象 每个处理者决定往不往下传,可以在中途截断;纯粹的链里只有一个处理者真正处理
装饰器(Decorator) 不改原对象,给它叠加功能 结构很像,也是同接口、持有下一个;但每层都做自己的事,并且一定往里传,最里面的原对象总会被调到。见第 10 章
命令(Command) 把一个请求封装成对象 命令管“请求本身是什么”,可以存起来、排队、撤销;责任链管“这个请求交给谁”。见第 19 章

两组都能用一个问题当场判断:

  • 责任链还是装饰器:问“每一层都一定往下传吗”。第 10 章的 Compressed 压缩完一定交给里面那层存,去掉它只是少了压缩,存还是会存,这是装饰器。本章的 Supervisor 批了就返回,Finance 根本没被调,这是责任链。23.6 的中间件介于两者之间:日志、计时每次都往下传,像装饰器;鉴权可以截断,这是责任链的做法。
  • 责任链还是命令:问“关心的是请求长什么样,还是谁来接”。第 19 章的 ChangePrice 带着要改的商品和价格,放进历史栈等撤销,是命令。本章的 RefundRequest 只是一份数据,重点在它沿链找到谁批,是责任链。两者可以一起用:把请求做成命令对象,再交给一条链决定谁执行它。

日志过滤链:一条日志记录 LogRecord 有级别(DEBUG、INFO、WARNING、ERROR)、模块名和消息。写三个过滤者:按级别(低于 INFO 的丢掉)、按模块(healthcheck 模块的丢掉)、按关键词(消息里含 password 的丢掉)。任一过滤者都可以丢弃记录,丢弃后后面的不再执行;三个都放过的,交给链尾的输出者打印出来。

依次送进五条记录,覆盖“被每个过滤者丢掉”和“顺利输出”两种情况,每条打印“输出”或“被谁丢弃”。Python 和 TS 各写一版。

提示:和本章的方向正好相反,这里“处理”是丢掉、“放过”才往下传,链尾是输出者而不是兜底审批人。按 23.7 的写法,让基类负责往下传,并返回是谁做了最后的决定,打印时就知道每条记录停在了哪一个过滤者。

  • 责任链把“谁来处理”拆成一串处理者:每个只判断归不归自己管,不归就交给下一个,发送者只认识链头。
  • 加一道环节是加一个处理者类,再插进需要它的链;链的顺序写在拼链的地方,每个渠道一行。
  • 顺序错了照样出事,只是错在一处、好查;处理者之间的顺序依赖要写清楚,最好有测试守着。
  • 链长了要让每个处理者留下记录,没人处理时报警,否则请求会悄无声息地消失在链里。
  • 纯粹的链只有一个处理者处理;中间件更接近管道,每层都做事、又能截断。和装饰器的区别:装饰器每层一定往里传,责任链可以停。