第 23 章 · 责任链(Chain of Responsibility)
一句话:把几个处理者连成一条链,请求从链头进去,每个处理者要么处理掉,要么交给下一个。 解决的痛:谁来处理写成一串
if,加一道检查要找准插在第几行;几个渠道各复制一份,漏改一份不报错,直到有人钻了空子。
23.1 一个场景
Section titled “23.1 一个场景”你在电商公司做售后系统。用户申请退款后,系统要决定这笔退款由谁来批:
- 100 元以下,客服直接批,系统马上放款;
- 100 元到 1000 元以下,主管批;
- 1000 元及以上,财务批。
退款申请从两个地方进来:App 和微信小程序。两边调的是同一个函数,传进一笔退款申请,返回“谁批准了”。本章只算审批结果,不连数据库,金额以元为单位,申请编号 R1、R2 这样排。
23.2 先按直觉写
Section titled “23.2 先按直觉写”金额分三档,从小往大判断,命中哪档就返回哪档:
from dataclasses import dataclass
@dataclassclass 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 元:财务批准三档规则一眼看完,判断顺序就是代码顺序。规则只有这几条、两个渠道也一样的时候,这样写清楚直接,大多数人第一版都这么写。
23.3 需求变了
Section titled “23.3 需求变了”运营提了三件事:一是最前面加一道风控,同一个用户 24 小时内第三次申请退款,不管金额多少都转人工审核;二是 VIP 用户 500 元以下的退款由客服直接批;三是两个渠道的流程分开,小程序没有会员体系,不设 VIP 通道。
小王要改三处:
- 风控:
RefundRequest加字段refunds_24h(这个用户 24 小时内已经退过几次,调用方查好填进来);函数第一行插一个判断,必须在所有金额判断之前,否则小额退款先被客服批掉,风控根本轮不到。 - VIP 通道:加字段
vip;插一个if req.vip and req.amount < 500,必须放在“小于 1000”那行之前。原来“按金额从小到大”的顺序被打乱了,判断条件开始交织:300 元的 VIP 申请同时满足两条,哪条在前由行号决定。 - 分渠道:函数复制成两份,
approve_refund_app和approve_refund_mini,App 和小程序的入口各调各的。以后改审批规则,两份都要改。
小王先把原来的函数复制一份给小程序(小程序不要 VIP 通道,这份就没再改);再去 App 那份加风控和 VIP。小程序那份就停在了复制时的样子。改完是这样:
@dataclassclass 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 那份。等风控同事发现有人专挑小程序、每天退好几笔小额,已经过去两周。
问题的形状是:每加一道审批环节,就要在每份函数里找准它该插的那一行;渠道有几个,同一条规则就要抄几遍,漏抄一份不报错。
图中 ①②③ 就是上面三处:黄色是 App 那份新插的两个判断,浅红是加了字段的 RefundRequest,红色加粗的框是那次事故:小程序那份从第一行就开始判断金额,第三次退款落进了“小于 100”。
23.4 用责任链模式重写
Section titled “23.4 用责任链模式重写”换个思路:每一档审批写成一个处理者(handler),它只回答一件事:“这笔申请归不归我管”。归它管,它给出决定;不归它管,交给下一个。几个处理者按顺序连起来,申请从第一个进去,走到有人处理为止。
基类 Handler 记住下一个处理者是谁,提供 set_next 和 pass_on;每个具体处理者实现 handle,返回一个决定 Decision,没人处理时返回 None。每个渠道的链在一个地方拼好:
from __future__ import annotations
from abc import ABC, abstractmethodfrom dataclasses import dataclass
@dataclassclass 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 风控转人工,小程序 风控转人工看黄色的两个处理者:风控和 VIP 通道是这次新加的,各是一个类。CustomerService、Supervisor、Finance 就是原来的三档金额判断,各搬进一个类,条件一个没变。下半部分是 make_chains 拼出的两条链,小程序那条少了 VipFastLane,但两条都从 RiskCheck 开头。
一笔 600 元的申请在 App 链上是这样走的:
每个处理者只看自己那一条条件,不满足就调 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 会说链长了以后怎么办。
责任链换来的是每一档审批是一个独立的类,链的顺序写在拼链的地方,每个渠道一行。代价是类多了,顺序的正确性仍然靠人保证,只是集中到了一处。
23.5 模式定义
Section titled “23.5 模式定义”GoF 对责任链的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
“避免发送者和接收者之间的耦合”,落到本章就是:App 的退款入口只调 chains["App"].handle(req),它不知道、也不需要知道这笔申请最后是客服、主管还是财务批的。哪档规则变了、加了风控,入口代码一行不改;23.3 里每改一次规则,两个入口调的函数都得跟着改。
看 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()就不再往外传。
23.6 真实系统里的例子
Section titled “23.6 真实系统里的例子”例一:Django 的中间件(Python)
Section titled “例一:Django 的中间件(Python)”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。
23.7 什么时候别用
Section titled “23.7 什么时候别用”- 处理者固定,只有一两个。比如“管理员直接通过,其他人走审核”,一个
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 通道。
23.8 容易混淆的模式
Section titled “23.8 容易混淆的模式”| 模式 | 意图 | 区别 |
|---|---|---|
| 责任链(Chain of Responsibility) | 让请求沿一条链传递,找到处理它的对象 | 每个处理者决定往不往下传,可以在中途截断;纯粹的链里只有一个处理者真正处理 |
| 装饰器(Decorator) | 不改原对象,给它叠加功能 | 结构很像,也是同接口、持有下一个;但每层都做自己的事,并且一定往里传,最里面的原对象总会被调到。见第 10 章 |
| 命令(Command) | 把一个请求封装成对象 | 命令管“请求本身是什么”,可以存起来、排队、撤销;责任链管“这个请求交给谁”。见第 19 章 |
两组都能用一个问题当场判断:
- 责任链还是装饰器:问“每一层都一定往下传吗”。第 10 章的
Compressed压缩完一定交给里面那层存,去掉它只是少了压缩,存还是会存,这是装饰器。本章的Supervisor批了就返回,Finance根本没被调,这是责任链。23.6 的中间件介于两者之间:日志、计时每次都往下传,像装饰器;鉴权可以截断,这是责任链的做法。 - 责任链还是命令:问“关心的是请求长什么样,还是谁来接”。第 19 章的
ChangePrice带着要改的商品和价格,放进历史栈等撤销,是命令。本章的RefundRequest只是一份数据,重点在它沿链找到谁批,是责任链。两者可以一起用:把请求做成命令对象,再交给一条链决定谁执行它。
23.9 练一练
Section titled “23.9 练一练”日志过滤链:一条日志记录 LogRecord 有级别(DEBUG、INFO、WARNING、ERROR)、模块名和消息。写三个过滤者:按级别(低于 INFO 的丢掉)、按模块(healthcheck 模块的丢掉)、按关键词(消息里含 password 的丢掉)。任一过滤者都可以丢弃记录,丢弃后后面的不再执行;三个都放过的,交给链尾的输出者打印出来。
依次送进五条记录,覆盖“被每个过滤者丢掉”和“顺利输出”两种情况,每条打印“输出”或“被谁丢弃”。Python 和 TS 各写一版。
提示:和本章的方向正好相反,这里“处理”是丢掉、“放过”才往下传,链尾是输出者而不是兜底审批人。按 23.7 的写法,让基类负责往下传,并返回是谁做了最后的决定,打印时就知道每条记录停在了哪一个过滤者。
23.10 小结
Section titled “23.10 小结”- 责任链把“谁来处理”拆成一串处理者:每个只判断归不归自己管,不归就交给下一个,发送者只认识链头。
- 加一道环节是加一个处理者类,再插进需要它的链;链的顺序写在拼链的地方,每个渠道一行。
- 顺序错了照样出事,只是错在一处、好查;处理者之间的顺序依赖要写清楚,最好有测试守着。
- 链长了要让每个处理者留下记录,没人处理时报警,否则请求会悄无声息地消失在链里。
- 纯粹的链只有一个处理者处理;中间件更接近管道,每层都做事、又能截断。和装饰器的区别:装饰器每层一定往里传,责任链可以停。