责任链模式:让请求沿着链条传递
责任链模式,说白了就是给请求找一条处理流水线。每个节点只关心自己能不能处理,处理不了就往后传。我在项目中见过太多人把判断逻辑堆在一个函数里,结果那个函数比我的胳膊还长——嗯,责任链就是用来解决这个问题的。
模式动机:为什么要用责任链?
想象一下你写了一个日志系统。日志有 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(客户端) | 创建链,并向链头发送请求 | 链的顺序搞反了,优先级高的放在后面 |
结构图:责任链的骨架
下面这张图展示了责任链的核心结构。你看,每个处理者都指向下一个,形成一个单向链表。请求从链头进入,一路往下走,直到被处理或者走到尽头。
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