C++ 实现责任链模式(Chain of Responsibility):从一堆 if-else 到可插拔的处理管道

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元 │   │ 无条件接住(兜底) │
 └───────────┘ └─────────┘ └──────────┘   └─────────────────┘

三个设计决策值得说明一下:

  1. handle() 用模板方法固化传递骨架。 把"能处理就处理、不能处理就往下传"这个流程写死在基类里,子类只回答两个问题:canApprove()(能不能处理)和 doApprove()(怎么处理)。这样传递逻辑就不会在子类里被写错、写漏。

  2. 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"))。

  3. 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
                        (传递到此终止,不再往下走)

注意几个特征:

  1. 请求的传递是"递归"而非"循环"。 每个节点调 next_->handle(req),形成一条调用链。这也意味着链不能太长,否则有栈溢出风险(几百个节点级别才需要担心,一般业务场景不会)。
  2. 判断权在节点自己手里。 submit() 完全不知道最终是谁批的,它只认识链头 chain
  3. 纯责任链里,请求最多被一个节点处理。 一旦某个节点 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 业务背景

一个温度采集程序:采集线程不停地从传感器读原始值,需要经过几道加工后才能入库:

  1. 校准:给原始读数加上偏移量(传感器有系统误差)
  2. 量程过滤 :超出物理量程(如 -40℃ ~ 150℃)的读数视为传感器故障,丢弃
  3. 指数平滑:滤掉抖动
  4. 投递:推入阻塞队列,交给消费线程入库

这个场景用责任链非常合适,因为每一步都符合"能处理就处理,然后决定要不要往下传"的形态,而且过滤节点需要短路能力(超量程的数据不该继续往下走)。

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 总结

回顾一下这篇文章的主线:

  1. 动机if / else if 堆出来的多级判断违反开闭原则,且无法运行时调整。责任链把"判断权"下放给每个节点,让新增处理环节从"改巨型函数"变成"加一个类"。
  2. 结构 :抽象处理者(Handler)用模板方法固化"能处理就处理,否则转交后继者"的骨架;子类只回答 canApprove()doApprove();客户端负责组装链。
  3. 两种形态纯责任链 里请求最多被一个节点处理(报销审批);不纯责任链里每个节点都处理自己那份并继续传递(日志分级、数据管道),还支持中途短路。
  4. 工程实践 :把责任链放在单一线程内执行,只在链尾跨线程,可以让有状态节点免于加锁;用 handle() 的返回值向客户端反馈处理结果(成功 / 被丢弃),比让节点自己打日志干净。
  5. 四个高频坑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 工程实践:跨线程数据采集管道

参考阅读:

相关推荐
一木 之林1 小时前
五、C++ 新特性、关键字与编译原理(进阶)(一)
c语言·开发语言·c++
2601_962298271 小时前
交易开拓者通常使用什么编程语言?这种语言如何帮助自动化交易?
java·c++·python·编程语言·自动化交易
码匠许师傅1 小时前
【设计模式精讲】23.备忘录模式(Memento)
c++·设计模式·软件工程·uml·备忘录模式
键盘会跳舞2 小时前
【C++多线程】thread_local 深度剖析:线程局部存储的生命周期与实现
c++·多线程·thread_local
whitelbwwww2 小时前
c++ 多线程
开发语言·c++·算法
努力努力再努力wz2 小时前
【Docker入门系列】:从架构演进到容器化:一文建立 Docker、虚拟化与 Namespace 的底层心智模型
运维·开发语言·数据结构·c++·docker·容器·架构
光电笑映2 小时前
Linux 线程编程:从进程、分页到线程控制与封装
linux·运维·服务器·c++
qinzechen2 小时前
本周科技行业热点汇总·2026第36周(2026年8月31日-9月6日)
c++·科技·算法
aqiu1111113 小时前
【C++ 代码分析与重构】常见 DFS 逻辑错误解析与修正
c++·重构·深度优先