Day 13:责任链模式(Chain of Responsibility Pattern)

2026-08-25 · 行为型模式 · C++17 / Qt 6 · 第 13 天(共 23 个 GoF 模式)

设计模式 责任链 行为型 Qt 事件系统

① 今日主题

责任链模式(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 中间件管道)。两者结构完全相同,区别只在于处理者"放行"的策略——前者处理完返回"已处理",后者处理完照样传给下一环。

④ UML / 角色关系

            ┌──────────────┐
            │   Client     │
            └──────┬───────┘
                   │ 发起请求
                   ▼
   ┌────────────────────────────┐
   │  Handler(抽象处理者)        │
   │  - next_ : 后继处理者        │
   │  + setNext(next)           │
   │  + handle(request)         │
   └─────────────┬──────────────┘
                 ▲(继承)
   ┌─────────────┴────────────────┐
   │                              │
┌──┴────────┐   ┌──┴────────┐   ┌──┴────────┐
│ 主管       │ → │ 经理       │ → │ 总监       │
│ limit=1000│   │ limit=5000│   │ limit=20000│
└───────────┘   └───────────┘   └───────────┘

四个角色:

① Handler(抽象处理者):定义 handle() 接口,并持有指向下一处理者的指针 next_;

② ConcreteHandler(具体处理者):实现"能否处理"的判定与"如何处理"的动作;处理不了就把请求转发给 next_;

③ Client(客户端 / 组装者):把各处理者按顺序串成链(setNext),并把请求只交给链头;

④ Request(请求对象):在链上传递的数据载体(报销单、事件、任务等)。

⑤ 最小 C++17 示例

用上面的"报销审批"场景实现一个完整可编译的 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 实战示例:事件过滤器链

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 接力——请求只有一个归宿,环节主动"抢"处理权。语义相反,别混用。

⑫ Qt 源码中的体现

① 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() 不再被调用。

问:责任链和装饰器都"串一串",本质区别是什么?

答:装饰器是层层包裹、所有层都执行,用于动态增强同一对象;责任链是接力传递、通常第一个能处理的环节接手即止,用于解耦"谁处理"。二者可组合:用装饰器装饰处理者,形成"增强版责任链"。

⑭ 今日练习(15-30 分钟)

需求:实现一个"HTTP 中间件管道"。一条请求依次经过 AuthMiddleware(校验 token,失败返回 401)→ RateLimitMiddleware(限流,超限返回 429)→ 业务处理器(返回 200 与结果)。任一中间件失败立即终止并返回错误码;成功后必须进入下一环节,最终一定返回一个结果。

提示 1:中间件接口可设计为 bool handle(Request&, int& errorCode)——返回 true 表示"继续传递",false 表示"终止";每个中间件持有 next,处理成功时调用 next->handle()。

提示 2:对照今天讲过的两种语义:日志链是"全放行",审批链是"处理即止"——中间件管道属于"要么全过、要么在某环失败终止"的混合体,想想哪一环该是兜底。

⑮ 今日总结

一句话记忆:请求像接力棒——谁能接谁接,没人接就到底。

代码特征信号(见到这些就该想到责任链):

① 类里存在 next_ / successor 成员指针,handle() 内有"能否处理"的判定;

② 处理函数返回 bool 或带有明确的"终止 / 放行"语义;

③ 客户端只调用链头,从不直接触碰具体处理者;

④ 新增处理环节不需要修改任何既有环节;

⑤ 链的组装集中在 main() / 工厂 / 配置中,而不是散落各调用点。

明日预告:Day 14 命令模式(Command Pattern)——把"动作"变成"对象",从而支持撤销重做、操作队列与宏命令;它也是 Qt QUndoStack(撤销栈)的地基。