C++ 实现责任链模式(Chain of Responsibility):从一堆 if-else 到可插拔的处理管道
最近在学习ffmpeg,代码中需要跨线程传递数据,所以用到了责任链模式进行传递,遂来学习一下责任链模式是如何进行处理的。
这篇文章从动机讲起,给出类结构设计 + 完整可编译的 C++11 代码,演示请求在链上是怎么一步步传递的,最后落到两个真实项目场景(日志分级处理、跨线程数据采集管道)。所有代码都实测编译通过,可以直接复制运行。
@TOC
1 为什么需要责任链模式
1.1 一段让人头大的 if-else
先看看最开始我写的报销审批函数(完整可编译,文件 naive_ifelse.cpp):
cpp
#include <iostream>
#include <string>
struct ExpenseRequest {
std::string applicant; // 申请人
std::string reason; // 报销事由
double amount; // 报销金额(元)
};
// 所有审批规则都挤在一个函数里
static void approve(const ExpenseRequest& req) {
if (req.amount <= 1000.0) {
std::cout << " 组长:批准 ¥" << req.amount << "(" << req.reason << ")。\n";
} else if (req.amount <= 5000.0) {
std::cout << " 经理:批准 ¥" << req.amount << "(" << req.reason << "),已抄送财务。\n";
} else if (req.amount <= 20000.0) {
std::cout << " 总监:批准 ¥" << req.amount << "(" << req.reason << "),已抄送财务与 CEO。\n";
} else {
std::cout << " [无人受理] ¥" << req.amount << " 超出全部审批人权限。\n";
}
}
int main(void) {
approve(ExpenseRequest{"张三", "市内打车费", 480.0});
approve(ExpenseRequest{"李四", "部门团建聚餐", 3200.0});
approve(ExpenseRequest{"王五", "服务器采购", 15000.0});
approve(ExpenseRequest{"赵六", "年度市场投放", 88000.0});
return 0;
}
这段代码短的时候看着还挺清爽,但需求一变就露馅了,问题有三:
| 问题 | 具体表现 |
|---|---|
| 违反开闭原则 | 新增"副总"这一级审批,必须修改 approve() 函数本身,而不是新增一个类 |
| 无法运行时调整 | "经理休假,跳过经理直接到总监"这种需求,只能加标志位 if (!managerOnLeave && ...),越堆越乱 |
| 分支互相耦合 | 三个分支共享同一个函数作用域,稍不注意就会引入互相影响的状态;这个函数也会无限膨胀 |
问题的本质是:"谁能处理这个请求"这个判断,和"怎么处理"这个动作,被硬编码在了同一个地方。责任链模式要做的,就是把它们拆开,让每个处理者自己决定"接不接",并且自己决定"接了之后要不要往下传"。
1.2 什么是责任链模式
责任链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。 ------ GoF《设计模式》
一句话概括:把"判断 + 处理"的权限下放给每个节点,节点之间只通过"后继者指针"相连,请求沿着链一路走下去。
这里有个很关键的细节值得单独拎出来说:请求的发送者完全不知道最终是谁处理的。发起请求的人只需要把请求丢给链头,剩下的事跟它没关系。这就是所谓的"解耦请求发送者和接收者"。
1.3 四个角色
| 角色 | 英文 | 职责 | 本文对应类 |
|---|---|---|---|
| 抽象处理者 | Handler | 定义处理接口,持有后继者指针 | Approver / Logger / Stage |
| 具体处理者 | ConcreteHandler | 实现"能否处理"和"如何处理" | GroupLeader / FileLogger / RangeStage 等 |
| 请求 | Request | 沿链传递的数据载体 | ExpenseRequest / Log / Sample |
| 客户端 | Client | 组装链,并向链头发起请求 | main() |
注意最后一行------组装链是客户端的职责,不是处理者的职责。这一点是责任链模式灵活性的来源:链的结构在运行时可以随意重组,处理者类本身一行代码都不用改。
2 适用场景与不适用场景
先看适合用责任链的场景特征:
| 场景特征 | 典型例子 |
|---|---|
| 多个对象都可能处理同一请求,但具体是谁要运行时才知道 | 审批流、客服工单升级、异常兜底 |
| 想在不明确指定接收者的情况下,向多个对象中的一个提交请求 | 事件冒泡、快捷键分发、Qt 的事件传递 |
| 处理者的集合和顺序需要动态变化 | 插件化过滤器、可配置的中间件 |
| 一个请求需要经过多道加工,每道各做各的 | 数据清洗管道、日志分级输出、Web 中间件 |
想消除冗长的 if / else if / else 分支 |
本文 1.1 节的反例 |
反过来,下面这些情况不要硬套责任链:
- 只有 1~2 个分支且未来不会变 :直接
if-else反而更清晰,别为了用模式而用模式。 - 要求请求 100% 被处理,且必须知道是谁处理的 :责任链天然允许"链尾无人受理",需要额外设计兜底(见 4.2 节的
onCannotHandle和 9.2 节)。 - 处理者之间需要频繁互相通信、共享复杂状态:那说明它们不该是独立的链节点,考虑合并成一个策略对象。
- 对性能极度敏感的热路径 :每个节点都是一次虚函数调用,超高频调用场景下要评估开销(可以改用函数对象数组 /
std::function列表)。
3 类结构设计
责任链的类结构非常固定,基本上就是"一个抽象基类 + 一堆子类",难点不在继承关系,而在基类里那个负责传递的骨架方法。
c
┌───────────────────────────────┐
│ Client (main) │
│ 负责组装链 + 发起请求 │
└───────────────┬───────────────┘
│ handle(req)
▼
┌────────────────────────────────────────────────────────────┐
│ Approver (抽象处理者) │
├────────────────────────────────────────────────────────────┤
│ # name_ : std::string │
│ # next_ : std::shared_ptr<Approver> ← 后继者指针 │
├────────────────────────────────────────────────────────────┤
│ + setNext(next) : Ptr ← 返回"刚设置的后继者" │
│ + handle(req) : void ← 模板方法:固化传递骨架 │
│ # canApprove(req): bool = 0 ← 子类回答:我能不能处理 │
│ # doApprove(req) : void = 0 ← 子类回答:我怎么处理 │
│ # onCannotHandle(req): void ← 钩子方法:链尾兜底 │
└────────────────────────────────────────────────────────────┘
△
┌────────────┼────────────┬─────────────────┐
│ │ │ │
┌───────────┐ ┌─────────┐ ┌──────────┐ ┌─────────────────┐
│GroupLeader│ │ Manager │ │ Director │ │ CfoFallback │
│ ≤1000元 │ │ ≤5000元 │ │ ≤20000元 │ │ 无条件接住(兜底) │
└───────────┘ └─────────┘ └──────────┘ └─────────────────┘
三个设计决策值得说明一下:
-
handle()用模板方法固化传递骨架。 把"能处理就处理、不能处理就往下传"这个流程写死在基类里,子类只回答两个问题:canApprove()(能不能处理)和doApprove()(怎么处理)。这样传递逻辑就不会在子类里被写错、写漏。 -
setNext()返回的是"刚设置的后继者"而不是this。 这是个很容易踩的坑,返回this的话a->setNext(b)->setNext(c)会把b覆盖掉,链条直接断掉(详见 [9.1 节](#9.1 节 "#91-setnext-%E8%BF%94%E5%9B%9E-next-%E8%BF%98%E6%98%AF-this"))。 -
用
std::shared_ptr而不是裸指针管理后继者。 链条的生命周期由首尾节点共同持有,用智能指针可以彻底避免"链断在半路导致野指针"的问题。
4 完整实现:多级报销审批(纯责任链)
业务规则:
| 审批人 | 权限上限 | 附加动作 |
|---|---|---|
组长 GroupLeader |
≤ ¥1000 | 直接批准 |
经理 Manager |
≤ ¥5000 | 批准后抄送财务 |
总监 Director |
≤ ¥20000 | 批准后抄送财务与 CEO |
CFO CfoFallback(可选兜底节点) |
无限制 | 转入专项审批队列 |
超过链上所有人权限的单子:要么走链尾兜底逻辑自动驳回归档,要么由显式兜底节点接住。两种方式下面都会演示。
下面按文件顺序逐段拆开讲,完整可编译的文件在 [4.5 节](#完整可编译的文件在 4.5 节 "#45-%E5%AE%8C%E6%95%B4%E5%8F%AF%E7%BC%96%E8%AF%91%E4%BB%A3%E7%A0%81"),想直接跑起来的可以跳过去复制。
4.1 请求对象 Request
cpp
struct ExpenseRequest {
std::string applicant; // 申请人
std::string reason; // 报销事由
double amount; // 报销金额(元)
};
注意:请求对象本身不需要知道"谁来处理它" ,它只是一个纯粹的数据载体。这正是解耦的关键------如果你发现请求类里出现了 Approver* 之类的字段,那说明设计跑偏了。
4.2 抽象处理者 Handler
cpp
class Approver {
public:
typedef std::shared_ptr<Approver> Ptr;
virtual ~Approver() {} // 多态基类必须有虚析构函数!
// 关键细节:返回"刚设置的后继者"而不是 this,
// 这样才能写成 head->setNext(a)->setNext(b) 一路往下串
Ptr setNext(Ptr next) {
next_ = next;
return next_;
}
// 统一处理接口:模板方法模式
// 把"能处理就处理,不能处理就往下传"这套流程固定在基类里,
// 子类只需回答两个问题:能不能处理?怎么处理?
void handle(const ExpenseRequest& req) {
if (canApprove(req)) { // 我能处理 -> 处理并终止传递(纯责任链)
doApprove(req);
return;
}
if (next_) { // 我处理不了 -> 转交后继者
std::cout << " " << name_ << ":金额超出我的权限,转交上级...\n";
next_->handle(req);
return;
}
onCannotHandle(req); // 已经到链尾 -> 兜底处理
}
protected:
explicit Approver(std::string name) : name_(name) {}
virtual bool canApprove(const ExpenseRequest& req) const = 0; // 能否处理
virtual void doApprove(const ExpenseRequest& req) const = 0; // 如何处理
// 链尾兜底:默认行为可被子类覆盖(钩子方法)
virtual void onCannotHandle(const ExpenseRequest& req) const {
std::cout << " [链尾兜底] 报销单(" << req.applicant << " / "
<< req.reason << " / ¥" << formatMoney(req.amount)
<< ")已超出全部审批人权限,系统自动驳回并归档。\n";
}
const std::string& name() const { return name_; }
private:
std::string name_;
Ptr next_; // 后继者(Successor)
};
三个要点:
virtual ~Approver() {}不能省。 通过基类指针shared_ptr<Approver>持有子类对象时,没有虚析构会导致子类析构函数不被调用,属于未定义行为。handle()是模板方法。 传递骨架在这里,子类改不了,也不会漏写if (next_)这种判断。onCannotHandle()是钩子方法(Hook)。 给了默认实现,子类可以不重写。它的作用是保证请求不会无声无息地消失 ------这一点在生产环境里非常重要,详见 [9.2 节](#9.2 节 "#92-%E5%BF%98%E4%BA%86%E9%93%BE%E5%B0%BE%E5%85%9C%E5%BA%95%E8%AF%B7%E6%B1%82%E8%A2%AB%E9%9D%99%E9%BB%98%E5%90%9E%E6%8E%89")。
4.3 具体处理者 ConcreteHandler
每个具体处理者只关心"自己的权限范围",完全不知道整条链长什么样:
cpp
class GroupLeader : public Approver {
public:
GroupLeader() : Approver("组长") {}
protected:
bool canApprove(const ExpenseRequest& req) const override {
return req.amount <= 1000.0;
}
void doApprove(const ExpenseRequest& req) const override {
std::cout << " " << name() << ":批准 ¥" << formatMoney(req.amount)
<< "(" << req.reason << ")。\n";
}
};
class Manager : public Approver {
public:
Manager() : Approver("经理") {}
protected:
bool canApprove(const ExpenseRequest& req) const override {
return req.amount <= 5000.0;
}
void doApprove(const ExpenseRequest& req) const override {
std::cout << " " << name() << ":批准 ¥" << formatMoney(req.amount)
<< "(" << req.reason << "),已抄送财务。\n";
}
};
class Director : public Approver {
public:
Director() : Approver("总监") {}
protected:
bool canApprove(const ExpenseRequest& req) const override {
return req.amount <= 20000.0;
}
void doApprove(const ExpenseRequest& req) const override {
std::cout << " " << name() << ":批准 ¥" << formatMoney(req.amount)
<< "(" << req.reason << "),已抄送财务与 CEO。\n";
}
};
// 显式链尾节点:无论什么请求都能"接住",保证请求一定有人收尾
class CfoFallback : public Approver {
public:
CfoFallback() : Approver("CFO(兜底节点)") {}
protected:
bool canApprove(const ExpenseRequest&) const override {
return true; // 无条件接住,作为显式链尾
}
void doApprove(const ExpenseRequest& req) const override {
std::cout << " " << name() << ":金额 ¥" << formatMoney(req.amount)
<< " 超出常规流程,转入 CFO 专项审批队列。\n";
}
};
想加一个"副总"?新建一个类,30 行,不改任何已有代码------这就是开闭原则最直观的收益。
4.4 客户端:组装链条
客户端只干两件事:组装链 和 发起请求。
cpp
// 便捷组装函数:把一组处理者按顺序串成一条链,返回链首
static Approver::Ptr linkChain(const std::vector<Approver::Ptr>& handlers) {
for (std::size_t i = 0; i + 1 < handlers.size(); ++i) {
handlers[i]->setNext(handlers[i + 1]);
}
return handlers.empty() ? Approver::Ptr() : handlers.front();
}
组装的几种写法,效果完全等价:
cpp
// 写法一:用 vector 批量串联(推荐,顺序一目了然)
std::vector<Approver::Ptr> handlers;
handlers.push_back(Approver::Ptr(new GroupLeader()));
handlers.push_back(Approver::Ptr(new Manager()));
handlers.push_back(Approver::Ptr(new Director()));
Approver::Ptr chain = linkChain(handlers);
// 写法二:链式调用(更紧凑)
Approver::Ptr leader(new GroupLeader());
leader->setNext(Approver::Ptr(new Manager()))
->setNext(Approver::Ptr(new Director()));
// 写法三:跳过经理(经理休假),一行调整,处理者类零改动
std::vector<Approver::Ptr> handlers3;
handlers3.push_back(Approver::Ptr(new GroupLeader()));
handlers3.push_back(Approver::Ptr(new Director())); // 注意:这里没有 Manager
Approver::Ptr chain3 = linkChain(handlers3);
写法三体现了责任链最大的价值:链的结构是运行时数据,不是编译期代码。
4.5 完整可编译代码
文件 expense_approval.cpp:
cpp
// =============================================================================
// 示例一:多级报销审批 ------ 纯责任链(Pure Chain of Responsibility)
//
// 业务规则:
// 组长 : 可审批 <= 1000 元
// 经理 : 可审批 <= 5000 元
// 总监 : 可审批 <= 20000 元
// 超过 20000 元:链上无人能处理,交给「链尾兜底」逻辑
//
// 编译: g++ -std=c++11 -Wall -Wextra expense_approval.cpp -o expense_approval
// =============================================================================
#include <iomanip>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#ifdef _WIN32
#include <windows.h> // Windows 控制台默认 GBK,输出中文会乱码,这里切到 UTF-8
#endif
// -----------------------------------------------------------------------------
// 1) 请求对象(Request):沿链传递的数据载体
// 注意:请求对象本身不需要知道"谁来处理它",这正是解耦的关键。
// -----------------------------------------------------------------------------
struct ExpenseRequest {
std::string applicant; // 申请人
std::string reason; // 报销事由
double amount; // 报销金额(元)
};
// 小工具:把金额格式化成带两位小数的字符串(避免不同编译器对 std::to_string 的兼容问题)
static std::string formatMoney(double value) {
std::ostringstream oss;
oss << std::fixed << std::setprecision(2) << value;
return oss.str();
}
// -----------------------------------------------------------------------------
// 2) 抽象处理者(Handler)
// 职责:
// a. 定义统一的处理接口 handle()(这里用「模板方法」固化传递骨架)
// b. 持有后继者指针,提供 setNext() 串联链条
// c. 提供链尾兜底 onCannotHandle()
// 实现要点:用 std::shared_ptr 管理后继者,杜绝裸指针与内存泄漏。
// -----------------------------------------------------------------------------
class Approver {
public:
typedef std::shared_ptr<Approver> Ptr;
virtual ~Approver() {}
// 设置后继者。
// 关键细节:这里返回的是"刚设置的后继者"而不是 this,
// 这样才能写成 head->setNext(a)->setNext(b) 一路往下串;
// 如果返回 *this,第二次调用会把 a 覆盖掉,链条就断了(常见踩坑点)。
Ptr setNext(Ptr next) {
next_ = next;
return next_;
}
const Ptr& next() const { return next_; }
// 统一处理接口:模板方法模式
// ------ 把"能处理就处理,不能处理就往下传"这套流程固定在基类里,
// 子类只需回答两个问题:能不能处理?怎么处理?
void handle(const ExpenseRequest& req) {
if (canApprove(req)) { // 我能处理 -> 处理并终止传递(纯责任链)
doApprove(req);
return;
}
if (next_) { // 我处理不了 -> 转交后继者
std::cout << " " << name_ << ":金额超出我的权限,转交上级...\n";
next_->handle(req);
return;
}
onCannotHandle(req); // 已经到链尾 -> 兜底处理
}
protected:
explicit Approver(std::string name) : name_(name) {}
// 子类实现:能否处理该请求(判断条件)
virtual bool canApprove(const ExpenseRequest& req) const = 0;
// 子类实现:具体怎么处理
virtual void doApprove(const ExpenseRequest& req) const = 0;
// 链尾兜底:默认行为可被子类覆盖(钩子方法)
virtual void onCannotHandle(const ExpenseRequest& req) const {
std::cout << " [链尾兜底] 报销单(" << req.applicant << " / "
<< req.reason << " / ¥" << formatMoney(req.amount)
<< ")已超出全部审批人权限,系统自动驳回并归档。\n";
}
const std::string& name() const { return name_; }
private:
std::string name_;
Ptr next_; // 后继者(Successor)
};
// -----------------------------------------------------------------------------
// 3) 具体处理者(Concrete Handler):组长 / 经理 / 总监
// 每个具体处理者只关心"自己的权限范围",完全不知道整条链长什么样。
// -----------------------------------------------------------------------------
class GroupLeader : public Approver {
public:
GroupLeader() : Approver("组长") {}
protected:
bool canApprove(const ExpenseRequest& req) const override {
return req.amount <= 1000.0;
}
void doApprove(const ExpenseRequest& req) const override {
std::cout << " " << name() << ":批准 ¥" << formatMoney(req.amount)
<< "(" << req.reason << ")。\n";
}
};
class Manager : public Approver {
public:
Manager() : Approver("经理") {}
protected:
bool canApprove(const ExpenseRequest& req) const override {
return req.amount <= 5000.0;
}
void doApprove(const ExpenseRequest& req) const override {
std::cout << " " << name() << ":批准 ¥" << formatMoney(req.amount)
<< "(" << req.reason << "),已抄送财务。\n";
}
};
class Director : public Approver {
public:
Director() : Approver("总监") {}
protected:
bool canApprove(const ExpenseRequest& req) const override {
return req.amount <= 20000.0;
}
void doApprove(const ExpenseRequest& req) const override {
std::cout << " " << name() << ":批准 ¥" << formatMoney(req.amount)
<< "(" << req.reason << "),已抄送财务与 CEO。\n";
}
};
// 显式链尾节点:无论什么请求都能"接住",保证请求一定有人收尾
class CfoFallback : public Approver {
public:
CfoFallback() : Approver("CFO(兜底节点)") {}
protected:
bool canApprove(const ExpenseRequest&) const override {
return true; // 无条件接住,作为显式链尾
}
void doApprove(const ExpenseRequest& req) const override {
std::cout << " " << name() << ":金额 ¥" << formatMoney(req.amount)
<< " 超出常规流程,转入 CFO 专项审批队列。\n";
}
};
// -----------------------------------------------------------------------------
// 4) 客户端(Client):负责"组装链"和"发起请求"
// 责任链的精髓:链的结构由客户端决定,运行时可以随时重组。
// -----------------------------------------------------------------------------
// 便捷组装函数:把一组处理者按顺序串成一条链,返回链首
static Approver::Ptr linkChain(const std::vector<Approver::Ptr>& handlers) {
for (std::size_t i = 0; i + 1 < handlers.size(); ++i) {
handlers[i]->setNext(handlers[i + 1]);
}
return handlers.empty() ? Approver::Ptr() : handlers.front();
}
static void submit(Approver::Ptr chain, const ExpenseRequest& req) {
std::cout << " >> 提交报销单:" << req.applicant << " | " << req.reason
<< " | ¥" << formatMoney(req.amount) << "\n";
if (chain) {
chain->handle(req);
} else {
std::cout << " [错误] 审批链为空,请求无人受理!\n";
}
std::cout << "\n";
}
int main() {
#ifdef _WIN32
SetConsoleOutputCP(CP_UTF8);
#endif
std::cout << "=========================================================\n";
std::cout << " 场景一:常规审批链 组长 -> 经理 -> 总监(无显式兜底)\n";
std::cout << "=========================================================\n";
// 组装链:客户端决定顺序,也决定链的长度
std::vector<Approver::Ptr> handlers;
handlers.push_back(Approver::Ptr(new GroupLeader()));
handlers.push_back(Approver::Ptr(new Manager()));
handlers.push_back(Approver::Ptr(new Director()));
Approver::Ptr chain = linkChain(handlers);
submit(chain, ExpenseRequest{"张三", "市内打车费", 480.0});
submit(chain, ExpenseRequest{"李四", "部门团建聚餐", 3200.0});
submit(chain, ExpenseRequest{"王五", "服务器采购", 15000.0});
submit(chain, ExpenseRequest{"赵六", "年度市场投放", 88000.0}); // 触发链尾兜底
std::cout << "=========================================================\n";
std::cout << " 场景二:追加显式兜底节点(CFO),请求一定有人接住\n";
std::cout << "=========================================================\n";
std::vector<Approver::Ptr> handlers2;
handlers2.push_back(Approver::Ptr(new GroupLeader()));
handlers2.push_back(Approver::Ptr(new Manager()));
handlers2.push_back(Approver::Ptr(new Director()));
handlers2.push_back(Approver::Ptr(new CfoFallback()));
Approver::Ptr chain2 = linkChain(handlers2);
submit(chain2, ExpenseRequest{"赵六", "年度市场投放", 88000.0});
std::cout << "=========================================================\n";
std::cout << " 场景三:运行时重组链(经理休假,请求直接升级到总监)\n";
std::cout << "=========================================================\n";
std::vector<Approver::Ptr> handlers3;
handlers3.push_back(Approver::Ptr(new GroupLeader()));
handlers3.push_back(Approver::Ptr(new Director())); // 跳过经理
Approver::Ptr chain3 = linkChain(handlers3);
submit(chain3, ExpenseRequest{"孙七", "客户招待费", 4200.0});
submit(chain3, ExpenseRequest{"周八", "展会物料", 26000.0});
std::cout << "=========================================================\n";
std::cout << " 场景四:链式写法 setNext()->setNext(),等价但更紧凑\n";
std::cout << "=========================================================\n";
Approver::Ptr leader(new GroupLeader());
leader->setNext(Approver::Ptr(new Manager()))
->setNext(Approver::Ptr(new Director()));
submit(leader, ExpenseRequest{"吴九", "办公用品", 880.0});
return 0;
}
4.6 编译与运行
Linux / macOS / MinGW:
bash
g++ -std=c++11 -Wall -Wextra expense_approval.cpp -o expense_approval
./expense_approval
实测运行结果 (MinGW g++ 4.9.2,-Wall -Wextra -pedantic 零告警):
markdown
=========================================================
场景一:常规审批链 组长 -> 经理 -> 总监(无显式兜底)
=========================================================
>> 提交报销单:张三 | 市内打车费 | ¥480.00
组长:批准 ¥480.00(市内打车费)。
>> 提交报销单:李四 | 部门团建聚餐 | ¥3200.00
组长:金额超出我的权限,转交上级...
经理:批准 ¥3200.00(部门团建聚餐),已抄送财务。
>> 提交报销单:王五 | 服务器采购 | ¥15000.00
组长:金额超出我的权限,转交上级...
经理:金额超出我的权限,转交上级...
总监:批准 ¥15000.00(服务器采购),已抄送财务与 CEO。
>> 提交报销单:赵六 | 年度市场投放 | ¥88000.00
组长:金额超出我的权限,转交上级...
经理:金额超出我的权限,转交上级...
[链尾兜底] 报销单(赵六 / 年度市场投放 / ¥88000.00)已超出全部审批人权限,系统自动驳回并归档。
=========================================================
场景二:追加显式兜底节点(CFO),请求一定有人接住
=========================================================
>> 提交报销单:赵六 | 年度市场投放 | ¥88000.00
组长:金额超出我的权限,转交上级...
经理:金额超出我的权限,转交上级...
总监:金额超出我的权限,转交上级...
CFO(兜底节点):金额 ¥88000.00 超出常规流程,转入 CFO 专项审批队列。
=========================================================
场景三:运行时重组链(经理休假,请求直接升级到总监)
=========================================================
>> 提交报销单:孙七 | 客户招待费 | ¥4200.00
组长:金额超出我的权限,转交上级...
总监:批准 ¥4200.00(客户招待费),已抄送财务与 CEO。
>> 提交报销单:周八 | 展会物料 | ¥26000.00
组长:金额超出我的权限,转交上级...
[链尾兜底] 报销单(周八 / 展会物料 / ¥26000.00)已超出全部审批人权限,系统自动驳回并归档。
=========================================================
场景四:链式写法 setNext()->setNext(),等价但更紧凑
=========================================================
>> 提交报销单:吴九 | 办公用品 | ¥880.00
组长:批准 ¥880.00(办公用品)。
5 请求是怎么在链上走的
上面这段输出看起来是线性的,理解责任链的关键在于看清楚请求在链上跳转时的调用栈。以场景一里"王五 / 服务器采购 / ¥15000.00"这一单为例:
php
submit(chain, req)
│
└─▶ GroupLeader::handle(req) amount=15000 > 1000 → canApprove=false
│ next_ 非空 → 打印"转交上级",递归调用
└─▶ Manager::handle(req) 15000 > 5000 → canApprove=false
│ next_ 非空 → 打印"转交上级",递归调用
└─▶ Director::handle(req) 15000 <= 20000 → canApprove=true
│
└─▶ doApprove(req) 打印"批准 ¥15000.00",return
(传递到此终止,不再往下走)
注意几个特征:
- 请求的传递是"递归"而非"循环"。 每个节点调
next_->handle(req),形成一条调用链。这也意味着链不能太长,否则有栈溢出风险(几百个节点级别才需要担心,一般业务场景不会)。 - 判断权在节点自己手里。
submit()完全不知道最终是谁批的,它只认识链头chain。 - 纯责任链里,请求最多被一个节点处理。 一旦某个节点
canApprove()返回true,处理完立即return,后面的节点根本不会被调用。
第 3 点正是"纯责任链"的定义。那么,如果我希望每个节点都处理一遍呢?这就是下一节的内容。
6 不纯责任链:一条请求被多个节点处理
6.1 场景与完整代码
GoF 定义里说的是"直到有一个对象处理它为止",但实际工程里更常见的形态是:每个节点都处理自己该做的那份,然后继续往下传 。这种变体叫做不纯责任链(Impure Chain of Responsibility)。
用日志分级处理来说明,最直观:
| 处理者 | 接球条件 | 处理动作 | 处理完是否继续传递 |
|---|---|---|---|
控制台 ConsoleLogger |
所有级别 | 打印到控制台 | 总是继续 |
文件 FileLogger |
仅 WARN 及以上 | 写入日志文件 | 总是继续 |
告警 AlertLogger |
仅 ERROR | 发送告警通知 | 由构造参数决定 |
为什么这是"不纯"责任链 :一条 ERROR 日志会被三个节点依次都处理一遍(打印 + 落盘 + 告警),而不是被某一个节点"消费"掉就结束。请求没有被独占,每个节点都在上面做自己的那份工作。
核心差别只有 handle() 的一个 return:
cpp
// 纯责任链:能处理就处理并 return,处理者唯一
if (canApprove(req)) { doApprove(req); return; }
// 不纯责任链:能处理也先处理,但**不 return**,继续往下走
if (match(log.level)) { write(log); }
if (next_) { next_->handle(log); }
文件 simple_impure.cpp(完整可编译):
cpp
// =============================================================================
// 不纯责任链·简化案例:日志分级处理链
//
// 场景:一条日志进来,依次经过「控制台 -> 文件 -> 告警」三个处理器。
// 控制台:什么级别都打印
// 文件 :只落盘 WARN 及以上
// 告警 :只在 ERROR 时发通知
//
// 为什么是"不纯"责任链?
// 一条 ERROR 日志会被三个节点**依次都处理一遍**(控制台打印 + 文件落盘 +
// 发送告警),而不是被某一个节点"消费"掉就结束。这就是不纯责任链的核心
// 特征:每个节点处理自己该做的那份工作,然后继续往下传。
//
// 编译: g++ -std=c++11 -Wall -Wextra simple_impure.cpp -o simple_impure
// =============================================================================
#include <iostream>
#include <memory>
#include <string>
#ifdef _WIN32
#include <windows.h>
// windows.h(wingdi.h)里有 #define ERROR 0,会和下面的枚举值 ERROR 撞名,
// 导致编译报 "expected identifier before numeric constant"。这里直接取消该宏。
#undef ERROR
#endif
// -----------------------------------------------------------------------------
// 请求对象:一条日志
// -----------------------------------------------------------------------------
enum class Level { DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4 };
struct Log {
Level level;
std::string msg;
};
static const char* levelName(Level lv) {
switch (lv) {
case Level::DEBUG: return "DEBUG";
case Level::INFO: return "INFO";
case Level::WARN: return "WARN";
case Level::ERROR: return "ERROR";
}
return "?";
}
// -----------------------------------------------------------------------------
// 抽象处理者
// -----------------------------------------------------------------------------
class Logger {
public:
typedef std::shared_ptr<Logger> Ptr;
virtual ~Logger() {}
// 返回"刚设置的后继者",支持 head->setNext(a)->setNext(b) 的链式串联
Ptr setNext(Ptr next) { next_ = next; return next_; }
// 不纯责任链的关键:先按自己的规则处理,再决定要不要继续往下传
void handle(const Log& log) {
if (match(log.level)) { // 这条日志归我管吗?
bool keepGoing = write(log);
if (!keepGoing) return; // 处理完要求终止 -> 短路,不再向后传递
}
if (next_) next_->handle(log); // 否则继续传给下一个
}
protected:
// 我关心这个级别吗?(子类的"接球条件")
virtual bool match(Level lv) const = 0;
// 具体处理;返回值 = 处理完之后是否还要继续传递
virtual bool write(const Log& log) const = 0;
private:
Ptr next_;
};
// -----------------------------------------------------------------------------
// 具体处理者
// -----------------------------------------------------------------------------
// 控制台:所有级别都打印,并且总是继续传递
class ConsoleLogger : public Logger {
protected:
bool match(Level) const override { return true; }
bool write(const Log& log) const override {
std::cout << " [控制台] " << levelName(log.level) << " " << log.msg << "\n";
return true;
}
};
// 文件:只落盘 WARN 及以上,并且总是继续传递
class FileLogger : public Logger {
protected:
bool match(Level lv) const override { return lv >= Level::WARN; }
bool write(const Log& log) const override {
std::cout << " [文件] 已写入日志文件:" << log.msg << "\n";
return true;
}
};
// 告警:只在 ERROR 时发通知;构造参数决定"发完是否终止传递"
class AlertLogger : public Logger {
public:
explicit AlertLogger(bool stopAfterAlert) : stopAfterAlert_(stopAfterAlert) {}
protected:
bool match(Level lv) const override { return lv >= Level::ERROR; }
bool write(const Log& log) const override {
std::cout << " [告警] 已发送告警通知:" << log.msg << " -> "
<< (stopAfterAlert_ ? "终止传递" : "继续传递") << "\n";
return !stopAfterAlert_; // true 表示"继续",所以这里取反
}
private:
bool stopAfterAlert_;
};
// -----------------------------------------------------------------------------
// 客户端
// -----------------------------------------------------------------------------
static void emit(Logger::Ptr chain, const Log& log) {
std::cout << ">> 收到日志 [" << levelName(log.level) << "] " << log.msg << "\n";
chain->handle(log);
std::cout << "\n";
}
int main(void) {
#ifdef _WIN32
SetConsoleOutputCP(CP_UTF8);
#endif
std::cout << "===== 演示 1:全员参与(不纯责任链的典型形态)=====\n";
std::cout << "链路:控制台 -> 文件 -> 告警(都不终止)\n\n";
Logger::Ptr chain(new ConsoleLogger());
chain->setNext(Logger::Ptr(new FileLogger()))
->setNext(Logger::Ptr(new AlertLogger(false)));
emit(chain, Log{Level::DEBUG, "查询缓存命中"});
emit(chain, Log{Level::WARN, "接口响应超时"});
emit(chain, Log{Level::ERROR, "数据库连接失败"});
std::cout << "===== 演示 2:告警后短路(同一套类,只改顺序 + 一个开关)=====\n";
std::cout << "链路:控制台 -> 告警(终止)-> 文件\n\n";
Logger::Ptr chain2(new ConsoleLogger());
chain2->setNext(Logger::Ptr(new AlertLogger(true)))
->setNext(Logger::Ptr(new FileLogger()));
emit(chain2, Log{Level::ERROR, "数据库连接失败"});
emit(chain2, Log{Level::WARN, "接口响应超时"});
return 0;
}
6.2 运行输出与解读
bash
g++ -std=c++11 -Wall -Wextra simple_impure.cpp -o simple_impure
./simple_impure
css
===== 演示 1:全员参与(不纯责任链的典型形态)=====
链路:控制台 -> 文件 -> 告警(都不终止)
>> 收到日志 [DEBUG] 查询缓存命中
[控制台] DEBUG 查询缓存命中
>> 收到日志 [WARN] 接口响应超时
[控制台] WARN 接口响应超时
[文件] 已写入日志文件:接口响应超时
>> 收到日志 [ERROR] 数据库连接失败
[控制台] ERROR 数据库连接失败
[文件] 已写入日志文件:数据库连接失败
[告警] 已发送告警通知:数据库连接失败 -> 继续传递
===== 演示 2:告警后短路(同一套类,只改顺序 + 一个开关)=====
链路:控制台 -> 告警(终止)-> 文件
>> 收到日志 [ERROR] 数据库连接失败
[控制台] ERROR 数据库连接失败
[告警] 已发送告警通知:数据库连接失败 -> 终止传递
>> 收到日志 [WARN] 接口响应超时
[控制台] WARN 接口响应超时
[文件] 已写入日志文件:接口响应超时
怎么读这份输出:
| 现象 | 说明 |
|---|---|
| DEBUG 只有控制台一行 | 文件(要求 ≥WARN)和告警(要求 ≥ERROR)都不匹配,跳过但链没断 |
| ERROR 有三行 | 三个节点依次都执行了------这就是"不纯"最直观的证据 |
| 演示 2 的 ERROR 只有两行 | 告警节点返回 false,请求在此短路,后面的文件节点没被执行 |
| 演示 2 的 WARN 走到文件 | 告警节点不匹配(WARN < ERROR),handle 直接跳到 if (next_) 继续传递 |
演示 2 想说明一件事:不纯责任链同时具备"全员处理"和"中途短路"两种能力,而切换它们只需要改客户端的组装顺序和一个构造参数,处理者类一个字都不用动。
7 实际项目应用:跨线程数据采集管道
前面两个例子都是单线程、纯演示性质的。这一节上一个更贴近真实工程的结构:责任链 + 生产者消费者。
7.1 业务背景
一个温度采集程序:采集线程不停地从传感器读原始值,需要经过几道加工后才能入库:
- 校准:给原始读数加上偏移量(传感器有系统误差)
- 量程过滤 :超出物理量程(如 -40℃ ~ 150℃)的读数视为传感器故障,丢弃
- 指数平滑:滤掉抖动
- 投递:推入阻塞队列,交给消费线程入库
这个场景用责任链非常合适,因为每一步都符合"能处理就处理,然后决定要不要往下传"的形态,而且过滤节点需要短路能力(超量程的数据不该继续往下走)。
7.2 数据流设计
perl
┌────────────┐ 校准 ┌──────────┐ 量程过滤 ┌──────────┐ 平滑 ┌──────────┐
│ 采集线程 │ ───────▶ │Calibrate │ ────────▶ │ Range │ ─────▶ │ Smooth │
│ (生产者) │ └──────────┘ └────┬─────┘ └────┬─────┘
└────────────┘ │ 超量程→丢弃 │
▼ ▼
(短路,不再后传) ┌──────────────┐
│ QueueSink │ 链尾
└──────┬───────┘
══════════════════════════════ 线程边界 ═══════════════════ │ push
▼
┌──────────────┐
│ SampleQueue │
└──────┬───────┘
│ pop
┌────────────┐ ┌──────▼───────┐
│ 消费线程 │ ◀───────────────────────────────────────────│ 入库/统计 │
└────────────┘ └──────────────┘
这里有个非常关键的设计决策 :整条责任链都在采集线程内 执行,只有链尾的 QueueSinkStage 跨越线程边界。带来的好处是------链上那些有状态的节点(比如平滑节点要记住上一次的值)不需要加任何锁,因为它们只被一个线程访问。
如果反过来,让责任链横跨多个线程,那每个节点的成员变量都得考虑加锁,代码会立刻复杂一个数量级。把责任链放在单一线程内,只在链尾跨线程,这是个很实用的经验。
7.3 核心代码
完整文件 thread_pipeline.cpp,这里贴出责任链相关的核心部分:
cpp
// -----------------------------------------------------------------------------
// 抽象处理者
// -----------------------------------------------------------------------------
class Stage {
public:
typedef std::shared_ptr<Stage> Ptr;
virtual ~Stage() {}
Ptr setNext(Ptr next) { next_ = next; return next_; }
// 返回值 = 数据是否成功走到链尾(false 表示中途被某个节点丢弃)
bool handle(Sample& s) {
if (!process(s)) return false; // 本节点决定终止 -> 短路
if (next_) return next_->handle(s); // 否则继续传递
return true; // 已到链尾
}
protected:
// 处理数据;返回 false 表示丢弃该数据并终止传递
virtual bool process(Sample& s) = 0;
private:
Ptr next_;
};
// 节点 1:校准。给原始读数加上偏移量,总是继续传递。
class CalibrateStage : public Stage {
public:
explicit CalibrateStage(double offset) : offset_(offset) {}
protected:
bool process(Sample& s) override {
s.value += offset_;
println(" [校准] #" + num(s.seq) + " -> " + fmt(s.value));
return true;
}
private:
double offset_;
};
// 节点 2:量程过滤。超出物理量程视为传感器故障,丢弃并短路。
class RangeStage : public Stage {
public:
RangeStage(double lo, double hi) : lo_(lo), hi_(hi) {}
protected:
bool process(Sample& s) override {
if (s.value < lo_ || s.value > hi_) {
println(" [量程] #" + num(s.seq) + " 数值 " + fmt(s.value)
+ " 超出 [" + fmt(lo_) + ", " + fmt(hi_) + "] -> 丢弃,终止传递");
return false; // 关键:短路,后面的节点不再执行
}
println(" [量程] #" + num(s.seq) + " 在量程内,放行");
return true;
}
private:
double lo_;
double hi_;
};
// 节点 3:指数平滑。注意这里有成员状态 last_,
// 因为它只被采集线程调用,所以不需要加锁(见 7.2 节的线程边界设计)。
class SmoothStage : public Stage {
public:
explicit SmoothStage(double alpha) : alpha_(alpha), last_(0.0), hasLast_(false) {}
protected:
bool process(Sample& s) override {
if (!hasLast_) {
last_ = s.value;
hasLast_ = true;
} else {
last_ = alpha_ * s.value + (1.0 - alpha_) * last_;
}
s.value = last_;
println(" [平滑] #" + num(s.seq) + " -> " + fmt(s.value));
return true;
}
private:
double alpha_;
double last_;
bool hasLast_;
};
// 节点 4:链尾。把加工好的数据推出线程边界。
class QueueSinkStage : public Stage {
public:
explicit QueueSinkStage(SampleQueue& queue) : queue_(queue) {}
protected:
bool process(Sample& s) override {
queue_.push(s); // 跨线程边界:数据离开采集线程
println(" [投递] #" + num(s.seq) + " 已推入队列(跨线程)");
return true;
}
private:
SampleQueue& queue_;
};
客户端组装与驱动:
cpp
int main(void) {
SampleQueue queue;
// 组装责任链:校准 -> 量程过滤 -> 平滑 -> 跨线程投递
Stage::Ptr pipeline(new CalibrateStage(0.5));
pipeline->setNext(Stage::Ptr(new RangeStage(-40.0, 150.0)))
->setNext(Stage::Ptr(new SmoothStage(0.5)))
->setNext(Stage::Ptr(new QueueSinkStage(queue)));
std::thread consumer(consumerMain, std::ref(queue));
// 模拟采集:第 3 条和第 6 条是传感器故障产生的离谱数值
const double raw[] = {25.0, 26.4, 999.9, 27.6, 24.9, -500.0, 26.2, 28.0};
const int n = sizeof(raw) / sizeof(raw[0]);
int delivered = 0;
int dropped = 0;
for (int i = 0; i < n; ++i) {
Sample s(i + 1, raw[i]);
println(" [采集线程] 原始 #" + num(s.seq) + " = " + fmt(s.value));
if (pipeline->handle(s)) { // 返回 false = 中途被丢弃
++delivered;
} else {
++dropped;
}
}
queue.close(); // 通知消费者:不会再有新数据了
consumer.join(); // 等待消费者处理完剩余数据
return 0;
}
注意这里 handle() 的返回值被客户端用来统计"投递 / 丢弃",这是不纯责任链在工程里很实用的一个变体:让链的返回值携带处理结果,比让节点自己写日志要干净得多。
7.4 运行输出
bash
g++ -std=c++11 -Wall -Wextra -pedantic -pthread thread_pipeline.cpp -o thread_pipeline
./thread_pipeline
less
===== 采集线程开始产生数据 =====
[采集线程] 原始 #1 = 25.00
[校准] #1 -> 25.50
[量程] #1 在量程内,放行
[平滑] #1 -> 25.50
[投递] #1 已推入队列(跨线程)
[采集线程] 原始 #2 = 26.40
[消费线程] 入库 #1 温度 25.50 ℃
[校准] #2 -> 26.90
[量程] #2 在量程内,放行
[平滑] #2 -> 26.20
[投递] #2 已推入队列(跨线程)
[采集线程] 原始 #3 = 999.90
[校准] #3 -> 1000.40
[量程] #3 数值 1000.40 超出 [-40.00, 150.00] -> 丢弃,终止传递
[采集线程] 原始 #4 = 27.60
[校准] #4 -> 28.10
...
===== 采集结束:投递 6 条,丢弃 2 条 =====
[消费线程] 队列已关闭,共入库 6 条,均值 26.52 ℃
(多线程输出顺序每次运行会略有不同,属正常现象。)
可以看到 #3(999.9℃)和 #6(-500℃)这两个故障值在校准之后就被量程节点拦下,短路终止,没有污染后面的平滑结果 ------如果用 if-else 写,这个"提前返回"的逻辑就得靠 continue 或者嵌套判断来表达,远不如责任链直观。
8 纯责任链 vs 不纯责任链
| 问题 | 纯(报销审批) | 不纯(日志链 / 数据管道) |
|---|---|---|
| 一条请求被几个节点处理? | 恰好 1 个 | 0 ~ N 个 |
处理完会 return 吗? |
会,立即终止 | 通常不会,继续传递 |
| 链尾兜底有必要吗? | 必须,否则请求丢失 | 可选 |
| 节点之间是什么关系? | 竞争(谁能谁上) | 协作(各做各的) |
| 典型应用 | 审批流、事件冒泡、异常兜底 | 过滤器管道、中间件、日志分级 |
再和几个容易混淆的模式做个区分:
| 模式 | 相似点 | 核心区别 |
|---|---|---|
| 装饰器 | 也是链式嵌套,也逐层处理 | 装饰器一定 会逐层调用到底并层层返回,且目的是"增强对象";责任链的节点可以中途终止传递 |
| 策略 | 都是消除 if-else |
策略是选一个 算法执行;责任链是依次询问多个处理者 |
| 观察者 | 都是一对多 | 观察者是广播 ,所有订阅者都会收到且无法中断;责任链是串行传递,可以中途短路 |
| 命令 | 都解耦发送者和接收者 | 命令模式封装的是"一个请求",责任链关注的是"谁来响应这个请求" |
一句话记忆:能中断、可短路、顺序可配 ------ 就是责任链。
9 总结
回顾一下这篇文章的主线:
- 动机 :
if / else if堆出来的多级判断违反开闭原则,且无法运行时调整。责任链把"判断权"下放给每个节点,让新增处理环节从"改巨型函数"变成"加一个类"。 - 结构 :抽象处理者(
Handler)用模板方法固化"能处理就处理,否则转交后继者"的骨架;子类只回答canApprove()和doApprove();客户端负责组装链。 - 两种形态 :纯责任链 里请求最多被一个节点处理(报销审批);不纯责任链里每个节点都处理自己那份并继续传递(日志分级、数据管道),还支持中途短路。
- 工程实践 :把责任链放在单一线程内执行,只在链尾跨线程,可以让有状态节点免于加锁;用
handle()的返回值向客户端反馈处理结果(成功 / 被丢弃),比让节点自己打日志干净。 - 四个高频坑 :
setNext()的返回值、链尾必须兜底、shared_ptr成环、Windows 下ERROR宏污染。
最后给一个选型建议:如果你的判断分支只有两三个、而且短期内不会变 ,老老实实写 if-else 就好,责任链是有成本的(多一层虚调用、多几个类)。但如果符合下面任意一条,那就果断上责任链:
- 分支会随需求不断增长
- 处理的顺序需要可配置
- 处理环节需要被复用(同一批节点组装成不同的链)
- 你想让"谁能处理"这个判断可以被单元测试单独覆盖
本文的所有示例代码都实测编译运行通过(MinGW g++ 4.9.2,-std=c++11 -Wall -Wextra -pedantic 零告警;MSVC 亦可),包含 4 个文件:
| 文件 | 说明 |
|---|---|
naive_ifelse.cpp |
反面教材:if-else 版本的多级审批 |
expense_approval.cpp |
纯责任链:多级报销审批(含 4 个演示场景) |
simple_impure.cpp |
不纯责任链:日志分级处理 |
thread_pipeline.cpp |
工程实践:跨线程数据采集管道 |
参考阅读:
- GoF《设计模式:可复用面向对象软件的基础》------ 责任链模式原始定义
- Chain of Responsibility | Refactoring.Guru ------ 有非常清晰的图示和多语言示例,中文版
- cppreference - std::shared_ptr ------ 本文链条持有方式的标准文档