责任链模式(Chain of Responsibility):把"谁能处理这个请求"的判定从调用方剥离出来,将多个处理者串成一条链,请求沿着链依次传递,直到某个处理者接管并处理,或到达链尾被兜底拒绝。一句话:请求像接力棒,谁能接谁接,没人接就到底。
先看一段没有责任链的"坏代码"。假设公司有一套报销审批流程:1000 元以内主管批,5000 元以内经理批,20000 元以内总监批,再往上要开董事会。最常见的写法是写一个大函数,用 if-else 层层判断:
// 坏味道:所有审批规则与处理逻辑堆在一个函数里
void handleReimbursement(double amount, const std::string& purpose) {
if (amount <= 1000.0) {
std::cout << "[主管] 批准:" << purpose << "(" << amount << "元)\n";
} else if (amount <= 5000.0) {
std::cout << "[经理] 批准:" << purpose << "(" << amount << "元)\n";
} else if (amount <= 20000.0) {
std::cout << "[总监] 批准:" << purpose << "(" << amount << "元)\n";
} else {
std::cout << "[董事会] 需要开会讨论:" << purpose << "\n";
}
}
这段代码的问题:
① 违反 SRP(单一职责):一个函数同时承载了"规则判定"(谁能批)、"处理动作"(打印结果)、"组织关系"(谁在谁前面)三类职责;
② 违反 OCP(开闭原则):公司新增一个"副总裁"审批级别,就必须修改这个函数——每加一级,if-else 就长一层,改动永远发生在"既有代码"上;
③ 高度耦合:调用方、审批规则、审批人三者焊死在一起,无法在运行时动态调整链的顺序或成员;
④ 难以测试:所有分支挤在一个函数里,测试 5 个级别就要走 5 条路径,任何一个分支改动都可能碰坏其他分支。
责任链模式的解法:把"主管""经理""总监"各自封装成一个处理者对象,每个对象只回答两个问题——我能不能处理?不能就传给下一个。新增审批级别 = 新增一个类 + 在组装处接上一环,既有代码一行不改。
责任链的核心是"请求的漂流":请求对象不再被一个"全知全能"的调度者分发,而是在一串处理者之间依次传递。每个处理者只有两个选择:
① 接手:判断自己有能力处理 → 处理并(通常)终止传递;
② 放手:判断自己处理不了 → 原样传给链上的下一个处理者。
生活类比:
· 客服工单升级:一线客服能解决就直接解决;解决不了就升级给二线技术;二线搞不定再升级给三线研发。工单就像请求,每一级都在做"能不能处理"的判断。
· 学校请假条:请 2 天假班主任签字即可;请 5 天要年级主任;请半个月要校长。请假条从班主任开始"沿链上递"。
· 流水线上的质检工位:每道工序只检查自己负责的项目,不合格就拦下,合格就放行到下一道。
注意责任链有两种语义:"谁先能处理谁处理、处理完即止"(审批链),以及"所有环节都要过一遍"(日志级别链、HTTP 中间件管道)。两者结构完全相同,区别只在于处理者"放行"的策略——前者处理完返回"已处理",后者处理完照样传给下一环。
┌──────────────┐
│ Client │
└──────┬───────┘
│ 发起请求
▼
┌────────────────────────────┐
│ Handler(抽象处理者) │
│ - next_ : 后继处理者 │
│ + setNext(next) │
│ + handle(request) │
└─────────────┬──────────────┘
▲(继承)
┌─────────────┴────────────────┐
│ │
┌──┴────────┐ ┌──┴────────┐ ┌──┴────────┐
│ 主管 │ → │ 经理 │ → │ 总监 │
│ limit=1000│ │ limit=5000│ │ limit=20000│
└───────────┘ └───────────┘ └───────────┘
四个角色:
① Handler(抽象处理者):定义 handle() 接口,并持有指向下一处理者的指针 next_;
② ConcreteHandler(具体处理者):实现"能否处理"的判定与"如何处理"的动作;处理不了就把请求转发给 next_;
③ Client(客户端 / 组装者):把各处理者按顺序串成链(setNext),并把请求只交给链头;
④ Request(请求对象):在链上传递的数据载体(报销单、事件、任务等)。
用上面的"报销审批"场景实现一个完整可编译的 C++17 程序。核心设计:Approver 抽象基类持有 std::shared_ptr 后继指针(保证链上各环节生命周期安全,避免悬垂指针);子类只实现自己的阈值与动作。
#include <iostream>
#include <memory>
#include <string>
// 请求对象:一张报销单
struct ExpenseRequest {
double amount; // 报销金额(元)
std::string purpose; // 用途说明
};
// 抽象处理者:审批人
class Approver {
public:
Approver(std::string name, double limit)
: name_(std::move(name)), limit_(limit) {}
virtual ~Approver() = default;
// 组装责任链:设置下一环
void setNext(std::shared_ptr<Approver> next) { next_ = std::move(next); }
// 处理请求:能批就批(终止链),不能批就传给下一环
void handle(const ExpenseRequest& req) {
if (req.amount <= limit_) {
approve(req); // 职责范围内:处理并终止
} else if (next_) {
std::cout << name_ << " 无权审批,转交 " << next_->name_ << "\n";
next_->handle(req); // 沿链下传
} else {
reject(req); // 链尾兜底:无人能处理
}
}
protected:
virtual void approve(const ExpenseRequest& req) {
std::cout << name_ << " 批准了 " << req.amount << " 元的报销("
<< req.purpose << ")\n";
}
virtual void reject(const ExpenseRequest& req) {
std::cout << name_ << " 无法处理," << req.amount << " 元的报销("
<< req.purpose << ")被驳回\n";
}
private:
std::string name_;
double limit_; // 本环节的审批上限
std::shared_ptr<Approver> next_; // 责任链中的后继者
};
// 三个具体处理者:每个只负责"自己的上限"这一件事
class Supervisor : public Approver {
public:
Supervisor() : Approver("主管", 1000) {}
};
class Manager : public Approver {
public:
Manager() : Approver("经理", 5000) {}
};
class Director : public Approver {
public:
Director() : Approver("总监", 20000) {}
};
int main() {
// 组装责任链:主管 -> 经理 -> 总监
auto supervisor = std::make_shared<Supervisor>();
auto manager = std::make_shared<Manager>();
auto director = std::make_shared<Director>();
supervisor->setNext(manager);
manager->setNext(director);
// 三笔不同金额的报销单,全部只交给"链头"
supervisor->handle(ExpenseRequest{800, "团建聚餐"});
std::cout << "----\n";
supervisor->handle(ExpenseRequest{3500, "差旅住宿"});
std::cout << "----\n";
supervisor->handle(ExpenseRequest{50000, "采购服务器"});
return 0;
}
程序输出:
主管 批准了 800 元的报销(团建聚餐)
----
主管 无权审批,转交 经理
经理 批准了 3500 元的报销(差旅住宿)
----
主管 无权审批,转交 经理
经理 无权审批,转交 总监
总监 无法处理,50000 元的报销(采购服务器)被驳回
编译运行:g++ -std=c++17 -Wall chain.cpp -o chain && ./chain。
为什么用 std::shared_ptr 而不是裸指针:链上后继者的生命周期由链整体决定,shared_ptr 保证"只要链还活着,环节就活着",杜绝悬垂指针与手动 new/delete;若改为单所有权场景,也可用 std::unique_ptr 持有后继、裸指针做观察(本系列 Day 12 代理模式的做法)。无论哪种,都不应出现裸 new。
Qt 自带的责任链设施就是 QObject 事件过滤器(event filter):任何 QObject 都可以通过 installEventFilter() 挂上一串过滤器,事件先被过滤器逐个询问,某个过滤器返回 true 表示"我处理了,链终止";返回 false 表示放行,继续问下一个。我们实现一条"审计 → 日志 → 拦截"三环节链。
chainfilter.h:
#pragma once
#include <QObject>
// 抽象处理者:事件过滤器基类——默认全部放行(return false)
class EventFilterBase : public QObject {
public:
using QObject::QObject;
protected:
bool eventFilter(QObject* watched, QEvent* event) override;
};
// 具体处理者 1:安全审计——只拦截鼠标按下,处理并终止链
class AuditFilter : public EventFilterBase {
protected:
bool eventFilter(QObject* watched, QEvent* event) override;
};
// 具体处理者 2:日志——记录所有事件,然后放行
class LogFilter : public EventFilterBase {
protected:
bool eventFilter(QObject* watched, QEvent* event) override;
};
// 具体处理者 3:拦截器——兜底拦截按键事件
class BlockerFilter : public EventFilterBase {
protected:
bool eventFilter(QObject* watched, QEvent* event) override;
};
chainfilter.cpp:
#include "chainfilter.h"
#include <QDebug>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
// 基类:不处理任何事件,放行给下一环
bool EventFilterBase::eventFilter(QObject* watched, QEvent* event) {
Q_UNUSED(watched);
Q_UNUSED(event);
return false; // false = 未处理,责任链继续
}
bool AuditFilter::eventFilter(QObject* watched, QEvent* event) {
if (event->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(event);
qDebug() << "[Audit] 审计:鼠标按下于" << me->position()
<< ",处理并终止链";
return true; // true = 已处理,链到此为止
}
return EventFilterBase::eventFilter(watched, event); // 不是我的职责
}
bool LogFilter::eventFilter(QObject* watched, QEvent* event) {
qDebug() << "[Log] 记录事件类型" << event->type() << ",放行";
return EventFilterBase::eventFilter(watched, event);
}
bool BlockerFilter::eventFilter(QObject* watched, QEvent* event) {
if (event->type() == QEvent::KeyPress) {
qDebug() << "[Blocker] 拦截按键事件,处理并终止链";
return true;
}
return EventFilterBase::eventFilter(watched, event);
}
main.cpp:
#include <QApplication>
#include <QPushButton>
#include "chainfilter.h"
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
// 过滤器对象必须先于被观察对象声明(后声明的先析构),
// 保证按钮销毁时过滤器还活着,避免悬挂指针
AuditFilter audit;
LogFilter log;
BlockerFilter blocker;
QPushButton button("点我,或按任意键(观察责任链)");
button.resize(280, 80);
// 组装责任链:后安装的过滤器先被询问
// 实际调用顺序:audit -> log -> blocker -> button
button.installEventFilter(&blocker);
button.installEventFilter(&log);
button.installEventFilter(&audit);
button.show();
return app.exec();
}
CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(chain_of_responsibility_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
add_executable(chain_demo
main.cpp
chainfilter.cpp
chainfilter.h
)
target_link_libraries(chain_demo PRIVATE Qt6::Widgets)
运行效果:点击按钮 → 只打印 [Audit] 审计:鼠标按下…处理并终止链(Log 收不到);按下键盘任意键 → 先 [Log] 记录事件类型 6…放行,再 [Blocker] 拦截按键事件…。鼠标事件被第一环消费,按键事件一路漂到第三环——责任链"终止"与"传递"两种语义一目了然。
以最小示例为准,请求在链中的完整旅程:
第 1 步|组装:main() 创建主管、经理、总监三个处理者,setNext() 依次串联成"主管 → 经理 → 总监",链头是主管;
第 2 步|发起:调用方只与链头对话:supervisor->handle(报销单),完全不认识经理和总监;
第 3 步|判定与传递:800 元单子:主管判定 800 ≤ 1000 成立 → approve() 打印"批准",链终止;3500 元单子:主管判定 3500 > 1000 → 打印"无权审批,转交 经理"并调用 next_->handle() → 经理判定 3500 ≤ 5000 → 批准,链终止;
第 4 步|链尾兜底:50000 元单子依次经过主管、经理、总监全部"无权",到达链尾时 next_ 为空 → reject() 打印"被驳回"——请求永远有明确归宿;
第 5 步(Qt 版)|鼠标事件:按钮收到 MouseButtonPress → Qt 先询问按钮的事件过滤器链:audit 命中 → 返回 true → 事件不再传给 log、blocker 与按钮自身;
第 6 步(Qt 版)|按键事件:KeyPress 到来 → audit 放行 → log 记录后放行 → blocker 命中并返回 true → 链终止,按钮的 keyPressEvent() 永远不会被调用。
解耦点:请求发送者与处理者彻底解耦——调用方只认识链头抽象,不认识任何具体处理者;处理者之间也只认识"下一个",互不认识、互不依赖。谁最终处理了请求,发送者无需知道。
扩展点 / OCP:新增一个审批级别 = 新增一个处理者类 + 在链组装处接上一环。既有处理者、调用方、链结构全部零改动——这正是"对扩展开放、对修改关闭"的标准实现。
DIP(依赖倒置):高层(main)依赖 Handler 抽象接口,具体处理者只在组装处出现一次,依赖方向全部指向抽象。
组合优于继承:链是运行时对象图而非编译期继承树:可以动态换序(先经理后主管)、动态增删环节(节日期间临时插入"值班总监")、甚至从配置文件读取链的构成。继承做不到这些。
单一职责:每个处理者只维护"自己的阈值与自己的动作",判定规则从 if-else 大杂烩里解放出来,分散到各自归属的类中。
可测试性:单个环节可以脱离整链独立单元测试(构造一个仅含该环节的链);也可以方便地构造不同组合做集成测试。
if-else 无限膨胀:审批级别从 3 级涨到 10 级,if-else 就长到 10 层;当判定条件变成多维(金额 + 部门 + 是否加急 + 项目类型),if 组合直接爆炸,读代码的人需要记住整棵决策树才能理解一条路径。
请求对象反向依赖处理者:另一种坏解法是把"谁能处理"写进请求对象("请找主管/经理/总监"),结果请求对象与处理者互相耦合——新增一个处理者,所有请求类都要跟着改。
调用方硬编码:每个调用点都 new 具体审批者,审批人改名、规则调整都会牵连整个工程重新编译;代码里散落着对具体类的直接依赖,抽象形同虚设。
链构建散落各处的反模式:即使引入了链,如果把"谁接谁"的组装逻辑复制到每个调用点,就会出现"这条链少一环"的隐蔽 bug——链的组装必须收敛到一处(main / 工厂 / 配置)。
反观 Qt 事件系统:如果没有过滤器链,QObject 就得在每个类里 switch 所有事件类型,Qt 就无法支撑自定义事件、全局钩子、第三方控件扩展——责任链是 Qt 可扩展性的地基之一。
适合(3-5 个场景):
① 多个对象都可能处理同一请求,具体由谁处理在运行时才确定——审批流、客服工单升级;
② 处理顺序 / 组合需要动态配置——可配置的审批链、插件化的中间件管道;
③ 请求必须依次经过一系列处理环节——日志级别链、HTTP 中间件(鉴权→限流→路由→业务);
④ 希望发送者完全不知道接收者是谁,实现可插拔——框架向第三方开放"处理点";
⑤ 处理环节经常增删,且要求不修改既有代码(OCP 场景)。
不适合(2-4 个场景):
① 链固定且很短——一两个 if 就能表达清楚时,建链是过度设计;
② 每个请求都要被所有环节处理且顺序无关——广播语义应该用观察者模式;
③ 链很长且性能极端敏感——每次请求 O(n) 遍历,命中率低时浪费明显;
④ 需要"谁处理了"的回执或撤销机制——责任链对结果回传很别扭,命令模式更合适。
过度设计提醒:两层 if 就能表达就别建链;链一旦建立,组装必须集中管理,否则"找谁处理"的问题会退化成"找谁组链"的新耦合。
| 对比项 | 责任链 | 装饰器 | 命令 |
|---|---|---|---|
| 请求去向 | 接力传递,通常第一个能处理的环节终止 | 层层包装,所有层都执行 | 封装为对象,交给单一执行者 |
| 结构 | 线性链(next 指针) | 递归套娃(外包内层) | 调用者 → 命令 → 接收者 |
| 控制点 | 每个环节自己决定处理 / 放行 | 外层决定何时增强、如何增强 | 调用者决定何时执行 / 撤销 |
| 典型目的 | 解耦"谁处理" | 动态增强既有对象功能 | 撤销、队列、宏命令、日志 |
| 相互组合 | 装饰器可以装饰处理者,形成"增强版责任链" | 命令对象也可以沿责任链传递 | |
vs 观察者:观察者是 1 对 N 广播——一个事件通知所有订阅者,订阅者被动接收;责任链是 1 对 1 接力——请求只有一个归宿,环节主动"抢"处理权。语义相反,别混用。
① QObject::installEventFilter() / eventFilters():每个 QObject 内部维护一张事件过滤器链表,后安装的过滤器先被询问,任一过滤器返回 true 即终止——教科书级的责任链实现(本文 ⑥ 节就是直接用它)。
② QApplication::notify():Qt 事件系统的总入口。事件先穿过目标对象的事件过滤器链,再调用 QObject::event()——全局兜底处理者。
③ 事件沿 parent 链冒泡:鼠标、键盘等自发事件在子控件未处理时会沿父链向上传播(焦点控件 → 父 widget → … → 顶层窗口),直到被某个环节消费——这是责任链在"控件树"上的体现。QWidget::event() 默认把事件分发给具体处理器(keyPressEvent 等),处理器不接受时返回 false,事件继续冒泡。
④ 工程实践:全局快捷键监听(对 qApp 安装过滤器)、全局鼠标钩子、自定义事件分发,用的都是这条链。理解责任链,就理解了 Qt 事件分发的"最后一公里"。
问:责任链模式解决什么问题?核心角色有哪些?
答:解耦"请求发送者"与"请求处理者",让多个处理者都有机会处理同一请求,且处理者集合可动态调整。四个角色:抽象处理者(定义接口 + 持有后继指针)、具体处理者(判定 + 处理或转发)、客户端(组装链 + 只向链头发起请求)、请求对象(链上传递的数据)。
问:责任链如何体现开闭原则?新增一个处理者需要改动哪些代码?
答:新增环节 = 新增一个处理者类 + 在链组装处(main/工厂/配置)接上一环。既有处理者、调用方、链结构全部无需修改。判定规则被封装进各环节内部,而不是散落在 if-else 里。
问:责任链"无人处理"怎么办?如何避免请求静默丢失?
答:三种兜底:① 链尾放一个默认处理者,吸收一切请求;② 链尾 next 为空时执行 reject/默认逻辑(本文做法);③ handle() 返回处理结果(bool / 错误码),由调用方检查。生产环境必须有兜底,否则请求会静默丢失。
问:Qt 中责任链体现在哪些地方?事件过滤器的顺序和返回值含义?
答:三处:installEventFilter 过滤器链(后安装的先被调用,返回 true 表示消费并终止,false 表示放行);未处理事件沿 parent 链冒泡;QApplication::notify() 作为全局入口与兜底。过滤器返回 true 后,目标对象自己的 event() 不再被调用。
问:责任链和装饰器都"串一串",本质区别是什么?
答:装饰器是层层包裹、所有层都执行,用于动态增强同一对象;责任链是接力传递、通常第一个能处理的环节接手即止,用于解耦"谁处理"。二者可组合:用装饰器装饰处理者,形成"增强版责任链"。
需求:实现一个"HTTP 中间件管道"。一条请求依次经过 AuthMiddleware(校验 token,失败返回 401)→ RateLimitMiddleware(限流,超限返回 429)→ 业务处理器(返回 200 与结果)。任一中间件失败立即终止并返回错误码;成功后必须进入下一环节,最终一定返回一个结果。
提示 1:中间件接口可设计为 bool handle(Request&, int& errorCode)——返回 true 表示"继续传递",false 表示"终止";每个中间件持有 next,处理成功时调用 next->handle()。
提示 2:对照今天讲过的两种语义:日志链是"全放行",审批链是"处理即止"——中间件管道属于"要么全过、要么在某环失败终止"的混合体,想想哪一环该是兜底。