责任链模式:让请求沿着链条传递

责任链模式,说白了就是给请求找一条处理流水线。每个节点只关心自己能不能处理,处理不了就往后传。我在项目中见过太多人把判断逻辑堆在一个函数里,结果那个函数比我的胳膊还长——嗯,责任链就是用来解决这个问题的。

模式动机:为什么要用责任链?

想象一下你写了一个日志系统。日志有 DEBUG、INFO、WARN、ERROR 四个级别。你可能会这么写:

void log(int level, const string& msg) {
    if (level >= DEBUG && level <= ERROR) {
        if (level == DEBUG) { /* 写调试日志 */ }
        else if (level == INFO) { /* 写信息日志 */ }
        else if (level == WARN) { /* 写警告日志 */ }
        else if (level == ERROR) { /* 写错误日志 */ }
    }
}

这段代码有什么问题?

  • 每个新级别都要改这个函数——违反开闭原则
  • 所有逻辑耦合在一起——难以单独测试
  • 处理顺序写死在代码里——无法灵活调整

我在一个支付网关项目中遇到过类似问题。不同的支付方式(微信、支付宝、银联)有不同的校验逻辑,一开始全写在一个类里,每次加新支付方式都要改核心代码,改一次出一次 bug。后来我重构成了责任链模式,每个支付方式一个处理器,新加支付方式只需要新增一个类,注册到链上就行。

核心动机:将请求的发送者和接收者解耦,让多个对象都有机会处理请求。把这些对象连成一条链,沿着链传递请求,直到有对象处理它为止。

模式定义与角色

责任链模式的定义其实很直白:

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。

这里面有几个关键角色:

角色 职责 我见过的坑
Handler(抽象处理者) 定义处理请求的接口,持有下一个处理者的引用 有人把具体处理逻辑也塞进抽象类,导致子类复用困难
ConcreteHandler(具体处理者) 实现自己的处理逻辑,决定是否处理或转发 忘记调用 next->handle(),链就断了
Client(客户端) 创建链,并向链头发送请求 链的顺序搞反了,优先级高的放在后面

结构图:责任链的骨架

下面这张图展示了责任链的核心结构。你看,每个处理者都指向下一个,形成一个单向链表。请求从链头进入,一路往下走,直到被处理或者走到尽头。

责任链模式结构图 Client Handler(抽象) + handleRequest() - next: Handler* ConcreteHandler1 ConcreteHandler2 ConcreteHandler3 请求 → Handler1 → Handler2 → Handler3 → ...

C++ 代码实现:从理论到实践

我们来实现一个日志处理系统。这个例子我在好几个项目里都用过,算是责任链的经典应用。

先定义抽象处理者:

#include <iostream>
#include <string>
#include <memory>

// 日志级别
enum class LogLevel {
    DEBUG,
    INFO,
    WARN,
    ERROR,
    FATAL
};

// 抽象处理者
class Logger {
protected:
    LogLevel level;
    std::shared_ptr<Logger> nextLogger;

public:
    explicit Logger(LogLevel lvl) : level(lvl), nextLogger(nullptr) {}
    
    void setNext(std::shared_ptr<Logger> next) {
        nextLogger = next;
    }
    
    void logMessage(LogLevel lvl, const std::string& message) {
        if (this->level <= lvl) {
            write(message);
        }
        if (nextLogger) {
            nextLogger->logMessage(lvl, message);
        }
    }
    
    virtual void write(const std::string& message) = 0;
    virtual ~Logger() = default;
};

然后实现具体处理者:

class ConsoleLogger : public Logger {
public:
    ConsoleLogger() : Logger(LogLevel::DEBUG) {}
    
    void write(const std::string& message) override {
        std::cout << "[Console] " << message << std::endl;
    }
};

class FileLogger : public Logger {
public:
    FileLogger() : Logger(LogLevel::INFO) {}
    
    void write(const std::string& message) override {
        std::cout << "[File] " << message << std::endl;
        // 实际项目中这里会写入文件
    }
};

class ErrorLogger : public Logger {
public:
    ErrorLogger() : Logger(LogLevel::ERROR) {}
    
    void write(const std::string& message) override {
        std::cout << "[Error] " << message << std::endl;
        // 发送邮件或短信通知
    }
};

客户端组装链并发送请求:

int main() {
    // 创建处理者
    auto consoleLogger = std::make_shared<ConsoleLogger>();
    auto fileLogger = std::make_shared<FileLogger>();
    auto errorLogger = std::make_shared<ErrorLogger>();
    
    // 组装责任链:console -> file -> error
    consoleLogger->setNext(fileLogger);
    fileLogger->setNext(errorLogger);
    
    // 发送请求
    consoleLogger->logMessage(LogLevel::DEBUG, "这是一条调试信息");
    consoleLogger->logMessage(LogLevel::INFO, "用户登录成功");
    consoleLogger->logMessage(LogLevel::ERROR, "数据库连接失败");
    
    return 0;
}

我的经验:组装链的时候,我习惯用一个工厂方法或者 Builder 来创建链。这样客户端只需要调用一个接口,不用关心链的具体结构。比如 auto logger = LoggerFactory::createChain();,改链的时候只改工厂,客户端代码纹丝不动。

请求处理链的变体

责任链有两种常见的变体,我在不同场景下都用过:

  • 纯责任链:一个请求最多被一个处理者处理。处理完了就停止传递。适合审批流程、权限校验。
  • 不纯责任链:每个处理者都可以处理,处理完继续往下传。就像上面的日志例子,DEBUG 日志既写控制台又写文件。

你想想看,这两种变体对应的是完全不同的业务需求。选错了,代码就会变得很奇怪。

避坑指南:我曾经在一个订单处理系统中用了纯责任链,但业务上要求多个处理器都要执行(比如校验库存、计算价格、记录日志)。结果链走到第一个处理器就停了,后面的逻辑全没跑。后来改成不纯责任链才解决问题。所以,动手之前先想清楚:是「找到一个处理者就停」,还是「所有处理者都要过一遍」?

责任链的优缺点

没有完美的模式,责任链也有它的适用场景和局限:

优点 缺点
降低耦合:发送者和接收者解耦 不能保证请求一定被处理(链尾没人接)
灵活性强:可以动态调整链的顺序和成员 调试困难:请求在链中传递,不容易定位问题
符合开闭原则:新增处理者不用改现有代码 性能损耗:请求可能经过多个无用的处理者
每个处理者职责单一,易于测试 链过长时,维护成本上升

我个人习惯在链的长度超过 5 个节点时,考虑是否真的需要这么长的链。有时候把链拆成多个子链,或者用组合模式来管理,会更清晰。

实际应用场景

责任链模式在真实项目中随处可见。我列几个我亲手用过的:

  • 日志系统:不同级别的日志输出到不同目标(控制台、文件、远程服务器)
  • 请求过滤:Web 框架中的过滤器链,每个过滤器做一件事(认证、授权、日志、压缩)
  • 审批流程:请假审批、报销审批,不同金额走不同审批人
  • 事件处理:GUI 中的事件冒泡,从子控件传到父控件

我记得有一次做一个消息推送系统,需要根据消息类型选择不同的推送渠道(短信、邮件、App 推送)。用责任链来实现,每个渠道一个处理器,新加渠道只需要新增一个类注册到链上。后来产品经理说「再加个微信模板消息」,我只花了半小时就加上了——这就是模式的力量。

一句话总结:责任链模式把「谁处理」的决策权从调用方转移到了链本身。调用方只需要把请求丢进链里,剩下的交给链自己去传递和决策。说白了,就是让代码学会「踢皮球」——但这个皮球踢得优雅、可扩展、好维护。


公众号:蓝海资料掘金营,微信deep3321