我一直觉得。
责任链这种东西。
更像是办公室政治。
不是贬义。
是它真的很像。
一个请求进来。
你不想让“前台”懂一切。
你也不想让“老板”处理每一张报销单。
那就只能。
一层一层往后递。
直到碰到那个。
“这事儿我能签”。
剩下的人。
只负责把它往后传。
这就叫责任链。
它的历史味道:从窗口消息到事件冒泡
GoF 1994 年把它写进书里的时候。
举的例子非常“上个世纪”。
GUI。
鼠标点一下。
到底是谁来处理?
窗口。
控件。
父窗口。
再到更上层的容器。
你别笑。
今天的前端。
事件冒泡还是这套。
你把按钮点了。
事件先问按钮。
按钮不接。
再问它爹。
它爹不接。
再往上问。
这就是责任链最朴素的形态。
有时候我会拿它去解释 Windows 的消息分发。
你写过 WndProc 或者 MFC。
就知道那种“消息一层层被翻译”的味道。
你只写你关心的。
别的都交给默认处理。
它看起来像是框架在帮你。
本质是。
框架在替你维护一条链。
先把烂摊子摆出来:一屏 if-else
我们做一个很常见的工程场景。
一个 HTTP 请求进来。
你要做几件事。
鉴权。
限流。
打日志。
参数校验。
再交给业务。
很多系统一开始都是这样写的。
struct Request {
std::string path;
std::string token;
int user_id = 0;
};
struct Response {
int status = 200;
std::string body;
};
Response handle(Request req) {
if (req.token.empty()) {
return {401, "missing token"};
}
if (req.path.rfind("/admin", 0) == 0 && req.user_id == 0) {
return {403, "forbidden"};
}
if (req.path == "/ping") {
return {200, "pong"};
}
return {404, "not found"};
}
代码能跑。
但你心里也明白。
这玩意儿会长胖。
你加一个“灰度开关”。
就要再插一段 if。
你加一个“AB 实验”。
又插一段 if。
最后你得到的不是业务。
是一个“判断树博物馆”。
责任链解决的不是功能
它解决的是。
“谁负责知道顺序”。
“谁负责插拔一段逻辑”。
“谁负责把请求交给下一个人”。
听起来抽象。
那就直接上一个最朴素的现代 C++ 写法。
我们先不用继承。
先用 std::function。
每个处理器只做一件事。
要么给出 Response。
要么说“我不管”。
#include <functional>
#include <optional>
#include <vector>
using MaybeResponse = std::optional<Response>;
using Handler = std::function<MaybeResponse(Request&)>;
Response run_chain(Request& req, const std::vector<Handler>& chain) {
for (const auto& h : chain) {
if (auto r = h(req)) {
return *r;
}
}
return {404, "not found"};
}
这段代码很像什么?
像中间件。
像过滤器。
像你在服务端见过的“管线”。
它的狠劲在于。
调用方只关心。
“给我一个链”。
链里有哪些人。
顺序怎么排。
都可以在别处决定。
一段代码,一段人情:鉴权、日志、业务
我们把链拼起来。
每个人只做自己那点事。
Handler auth = [](Request& r) -> MaybeResponse {
if (r.token.empty()) {
return Response{401, "missing token"};
}
return std::nullopt;
};
Handler ping = [](Request& r) -> MaybeResponse {
if (r.path == "/ping") {
return Response{200, "pong"};
}
return std::nullopt;
};
Handler business = [](Request& r) -> MaybeResponse {
if (r.path == "/order") {
return Response{200, "ok"};
}
return std::nullopt;
};
你会发现。
这些 handler 没有“框架味”。
没有继承。
没有虚函数。
甚至没有 next 指针。
但它已经是责任链了。
再来把它们排队。
std::vector<Handler> chain = {auth, ping, business};
Request req{.path = "/ping", .token = "t"};
Response resp = run_chain(req, chain);
这时候。
你想插一个“限流”。
就把它塞进 vector。
你想把“日志”放到最前面。
就换个顺序。
你甚至可以在测试里。
只拼一条短链。
让测试更像测试。
但老程序员会皱眉:这玩意儿最容易把人绕晕
责任链的坑。
从来不是“怎么写”。
是“怎么查”。
你线上看到一个 403。
你想知道。
是谁返回的。
在哪个条件下返回的。
链太长。
你就会开始怀疑人生。
所以责任链在工程里。
必须搭配一件东西。
可观测性。
别太高深。
就是能把链路跑一遍。
留下点脚印。
我们给 Request 加一个最简单的轨迹。
struct Request {
std::string path;
std::string token;
int user_id = 0;
std::string trace;
};
每个处理器动手之前。
把自己的名字写进去。
Handler auth = [](Request& r) -> MaybeResponse {
r.trace += "auth>";
if (r.token.empty()) {
return Response{401, "missing token"};
}
return std::nullopt;
};
Handler business = [](Request& r) -> MaybeResponse {
r.trace += "business>";
if (r.path == "/order") {
return Response{200, "ok"};
}
return std::nullopt;
};
你别嫌它土。
这种土办法。
救过很多人的命。
因为它把“到底走到了哪里”。
变成了一个字符串。
你一眼就能看见。
什么时候该用它
当你的系统里。
“处理步骤会变”。
“处理步骤要按场景组合”。
“你不想让调用方知道具体是谁处理”。
责任链就很自然。
中间件。
事件分发。
日志与鉴权。
审批流。
都是它的舒适区。
但如果你的流程。
必须严格可追溯。
必须严格可回放。
而且步骤之间强依赖。
那你就要小心。
责任链会让你“插拔很爽”。
也会让你“定位很痛”。
有一句老话。
我很喜欢。
“能把事办成的架构,很多。”
“能把事说清楚的架构,才值钱。”
责任链用得好。
它能把系统说清楚。
用得不好。
它会把系统藏起来。
最后只剩一句。
“我也不知道是谁处理的。”