77、STL与责任链:责任链模式、请求处理、动态链构建

责任链模式,说白了就是让一堆处理对象排成一条队。请求从队头进来,挨个问:「你能处理我吗?」谁行谁上,不行就往后传。我刚开始接触这个模式时觉得挺简单,后来在项目中才发现,真正用好它,关键在于「链」的动态构建能力。

你想想看,如果链是写死的,那跟if-else有啥区别?STL容器给了我们动态管理节点的能力,这才是责任链的精髓所在。

责任链的核心思想

责任链模式解决的是「请求发送者」和「请求接收者」之间的解耦问题。每个处理者只关心自己能不能处理,不关心前面是谁、后面是谁。

我习惯把责任链比作流水线上的工位。每个工位只做一件事,做不了就传给下一个。这样新增一个工位,完全不影响其他工位。

核心三要素:
  • 处理者接口:定义统一的处理方法和设置后继者的接口
  • 具体处理者:实现自己的处理逻辑,决定是否传递
  • 链的管理:动态构建、调整处理顺序

STL如何助力责任链

传统责任链实现中,每个节点都持有下一个节点的指针。这种静态链表的缺点很明显:增删节点需要修改代码,运行时无法调整顺序。

STL的std::vectorstd::liststd::unique_ptr等容器和智能指针,让我们可以动态管理处理者列表。我个人最喜欢用std::vector<std::unique_ptr<Handler>>,既保证了所有权清晰,又支持随机访问。

我的经验:用vector管理责任链节点,比手写链表更安全、更灵活。迭代器失效问题?只要不在遍历时修改容器就行。

代码实战:日志级别过滤器

来看一个实际例子。假设我们要实现一个日志系统,不同级别的日志由不同的处理器负责。DEBUG日志写入调试文件,INFO日志打印到控制台,ERROR日志发送邮件告警。

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

// 日志级别枚举
enum class LogLevel {
    DEBUG,
    INFO,
    WARNING,
    ERROR
};

// 处理者基类
class LogHandler {
public:
    virtual ~LogHandler() = default;
    virtual void handle(const std::string& message, LogLevel level) = 0;
};

// 具体处理者:DEBUG级别
class DebugHandler : public LogHandler {
public:
    void handle(const std::string& message, LogLevel level) override {
        if (level == LogLevel::DEBUG) {
            std::cout << "[DEBUG] " << message << std::endl;
        }
        // 不传递,DEBUG只处理自己的级别
    }
};

// 具体处理者:INFO级别
class InfoHandler : public LogHandler {
public:
    void handle(const std::string& message, LogLevel level) override {
        if (level == LogLevel::INFO) {
            std::cout << "[INFO] " << message << std::endl;
        }
    }
};

// 具体处理者:ERROR级别
class ErrorHandler : public LogHandler {
public:
    void handle(const std::string& message, LogLevel level) override {
        if (level == LogLevel::ERROR) {
            std::cerr << "[ERROR] " << message << " - 已发送告警邮件" << std::endl;
        }
    }
};

// 责任链管理器
class LoggerChain {
private:
    std::vector<std::unique_ptr<LogHandler>> handlers;

public:
    // 动态添加处理者
    template<typename T, typename... Args>
    void addHandler(Args&&... args) {
        handlers.emplace_back(std::make_unique<T>(std::forward<Args>(args)...));
    }

    // 处理日志
    void log(const std::string& message, LogLevel level) {
        for (auto& handler : handlers) {
            handler->handle(message, level);
        }
    }
};

int main() {
    LoggerChain chain;
    chain.addHandler<DebugHandler>();
    chain.addHandler<InfoHandler>();
    chain.addHandler<ErrorHandler>();

    chain.log("这是一条调试信息", LogLevel::DEBUG);
    chain.log("用户登录成功", LogLevel::INFO);
    chain.log("数据库连接失败", LogLevel::ERROR);

    return 0;
}

这段代码里,每个处理器只处理自己关心的级别。注意看,我没有让处理器之间互相持有指针,而是用vector统一管理。这样做的好处是:

  • 添加新处理器只需调用addHandler,不改现有代码
  • 可以运行时决定处理顺序
  • 内存管理由unique_ptr自动搞定

动态链构建的进阶技巧

上面的例子是「广播式」责任链——每个处理器都收到请求。但经典责任链是「传递式」的:一个处理器处理完,可以选择是否传给下一个。

我曾经在项目中遇到一个需求:用户提交的表单需要经过多个校验器,每个校验器通过后才传给下一个。一旦某个校验失败,立即返回错误,不再继续。

这种场景下,我让每个处理器返回一个布尔值,表示「是否继续传递」:

class Validator {
public:
    virtual ~Validator() = default;
    virtual bool validate(const std::string& input) = 0;
};

class ChainValidator {
    std::vector<std::unique_ptr<Validator>> validators;
public:
    void add(std::unique_ptr<Validator> v) {
        validators.push_back(std::move(v));
    }

    bool validate(const std::string& input) {
        for (auto& v : validators) {
            if (!v->validate(input)) {
                return false;  // 一旦失败,立即中断
            }
        }
        return true;
    }
};
注意:这种「短路求值」的责任链,处理顺序至关重要。我曾经因为把「非空校验」放在「格式校验」后面,导致空指针异常。建议把前置条件校验放在链的前端。

责任链的适用场景

场景 说明 推荐实现
日志过滤 不同级别日志不同处理 广播式,每个处理器独立
输入校验 多个校验规则依次执行 传递式,短路求值
权限检查 逐级检查用户权限 传递式,找到匹配即停止
事件处理 UI事件冒泡 传递式,可中途拦截

SVG:责任链模式结构图

责任链模式结构图 请求发起者 处理者A:检查非空 处理者B:检查格式 处理者C:检查权限 处理流程 请求依次经过每个处理者 任一处理者返回false 则立即终止 动态构建 使用vector管理 运行时增删节点 顺序可调整

避坑指南

我踩过不少责任链的坑,挑几个典型的说说:

  • 链的循环问题:我曾经在动态构建时不小心让A指向B、B指向C、C又指向A,结果死循环。用vector管理就不会有这个问题。
  • 性能开销:如果链很长(比如上百个处理器),每次请求都遍历一遍,性能会下降。我建议用std::partitionstd::sort对处理器按优先级排序,高频处理器放前面。
  • 线程安全:多线程环境下,动态修改链容器需要加锁。我习惯用std::shared_mutex做读写分离。
小技巧:如果责任链中的处理器有优先级,可以用std::priority_queue来管理。不过要注意,优先队列不支持遍历,需要自己封装。

责任链模式配上STL,就像给流水线装上了可编程控制器。你可以在运行时决定谁来处理、处理顺序、是否中断。这种灵活性,在复杂业务系统中特别有用。

嗯,最后说一句:不要为了用模式而用模式。如果只有两三个处理分支,简单的if-else反而更清晰。责任链的优势在于「动态」和「扩展」,用对地方才是好代码。


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