C++职责链模式实战:从设计原理到日志框架应用
1. 项目概述:为什么我们需要职责链模式?
在C++项目里,尤其是处理那些流程化、多分支的业务逻辑时,我们经常会遇到一种头疼的情况:一个请求(比如一个用户操作、一个事件、一个数据包)需要经过一系列的处理步骤,而每个步骤的处理者(Handler)都可能根据自身条件决定是“处理”还是“甩锅”给下一个。如果你用一堆if-else或者switch-case把这些逻辑硬编码在一起,代码很快就会变得臃肿不堪,难以维护和扩展。每次加一个新的处理步骤,你都得去修改那个已经长得吓人的条件判断函数,这违反了开闭原则,也埋下了bug的种子。
职责链模式(Chain of Responsibility Pattern)就是为了优雅地解决这个问题而生的。它的核心思想很简单:把多个可能处理请求的对象连成一条链,请求沿着这条链传递,直到有一个对象处理它为止。发送者不需要知道最终是谁处理了请求,处理者之间也无需知道彼此的存在,它们只关心自己能不能处理,以及下一个处理者是谁。这种解耦带来的灵活性是巨大的,你可以在运行时动态地调整链的顺序,增加或删除处理者,而不用动核心的业务逻辑。
从你提供的热词来看,很多朋友在找“C++设计模式”、“C++面试题”、“C++八股文”,职责链模式绝对是其中的高频考点和实用工具。它不像单例模式那样随处可见,但在处理审批流、事件过滤、日志记录、异常捕获、中间件管道等场景时,是不可多得的设计利器。接下来,我就结合自己十多年的C++开发经验,从应用场景、实现细节到避坑指南,带你彻底搞懂这个模式。
2. 职责链模式的核心结构与C++实现拆解
理解一个设计模式,最好的方式就是先看它的骨架,再填充血肉。职责链模式通常包含以下几个关键角色:
- 处理者接口(Handler):定义处理请求的接口,以及设置下一个处理者的方法。这是所有具体处理者的契约。
- 抽象处理者(AbstractHandler):一个可选的基类,用于实现链的构建逻辑(即设置和传递下一个处理者的默认行为),避免每个具体处理者都重复写这段代码。
- 具体处理者(Concrete Handlers):实现处理者接口的类。每个具体处理者都包含自己的业务逻辑,决定自己能否处理某个请求。如果能,就处理并返回结果;如果不能,就调用父类或持有的下一个处理者的处理方法,将请求传递下去。
- 客户端(Client):负责组装职责链,并将请求发送给链上的第一个处理者。它通常只与链的头部打交道。
2.1 基础实现代码解析
我们先用一个经典的“请假审批”场景来还原一个最基础的实现。假设请假天数不同,需要不同级别的领导审批:1-3天组长批,4-7天经理批,8天以上总监批。
#include <iostream> #include <string> #include <memory> // 1. 处理者接口 class LeaveHandler { public: virtual ~LeaveHandler() = default; // 设置下一个处理者 virtual void setNext(std::shared_ptr<LeaveHandler> next) = 0; // 处理请假请求 virtual void handleRequest(int days) = 0; }; // 2. 抽象处理者(实现链的传递逻辑) class AbstractLeaveHandler : public LeaveHandler { protected: std::shared_ptr<LeaveHandler> nextHandler_; // 指向下一个处理者的智能指针 public: AbstractLeaveHandler() : nextHandler_(nullptr) {} void setNext(std::shared_ptr<LeaveHandler> next) override { nextHandler_ = next; } // 默认的传递行为:如果自己处理不了,就交给下一个 void handleRequest(int days) override { if (nextHandler_) { nextHandler_->handleRequest(days); } else { // 链的末端,无人处理 std::cout << "请假天数[" << days << "]天,无人有权审批,请核实流程。\n"; } } }; // 3. 具体处理者:组长 class GroupLeaderHandler : public AbstractLeaveHandler { public: void handleRequest(int days) override { if (days <= 3) { std::cout << "组长批准了 " << days << " 天的请假。\n"; // 处理完成,不再传递 } else { std::cout << "组长无权审批 " << days << " 天的请假,已提交上级。\n"; // 自己处理不了,调用父类方法传递给下一个 AbstractLeaveHandler::handleRequest(days); } } }; // 3. 具体处理者:经理 class ManagerHandler : public AbstractLeaveHandler { public: void handleRequest(int days) override { if (days <= 7) { std::cout << "经理批准了 " << days << " 天的请假。\n"; } else { std::cout << "经理无权审批 " << days << " 天的请假,已提交上级。\n"; AbstractLeaveHandler::handleRequest(days); } } }; // 3. 具体处理者:总监 class DirectorHandler : public AbstractLeaveHandler { public: void handleRequest(int days) override { if (days <= 15) { // 假设总监最多批15天 std::cout << "总监批准了 " << days << " 天的请假。\n"; } else { std::cout << "总监无权审批 " << days << " 天的请假。\n"; AbstractLeaveHandler::handleRequest(days); // 传递给“空”,即无人处理 } } }; // 4. 客户端 int main() { // 创建处理者 auto groupLeader = std::make_shared<GroupLeaderHandler>(); auto manager = std::make_shared<ManagerHandler>(); auto director = std::make_shared<DirectorHandler>(); // 组装职责链:组长 -> 经理 -> 总监 groupLeader->setNext(manager); manager->setNext(director); // 注意:总监后面没有设置next,它就是链的末端 std::cout << "=== 请假审批流程测试 ===\n"; // 发送请求给链的头部(组长) groupLeader->handleRequest(2); // 组长批 groupLeader->handleRequest(5); // 经理批 groupLeader->handleRequest(10); // 总监批 groupLeader->handleRequest(20); // 无人能批 return 0; }代码要点与心法:
- 智能指针管理生命周期:我使用了
std::shared_ptr来管理处理者对象。这在实际项目中非常重要,能有效避免内存泄漏。链中的对象相互引用,使用原始指针很容易出错。你也可以根据所有权关系选择std::unique_ptr,但设置next时需要小心转移语义。 - 清晰的职责边界:每个
Concrete Handler的handleRequest方法里,if判断的条件就是它的“职责范围”。处理完成后直接返回,不再调用父类的传递方法,这是“处理并终止传递”的标准做法。 - 可选的末端处理:在抽象基类
AbstractLeaveHandler的handleRequest中,当nextHandler_为空时,我选择输出一条提示。这是一种“兜底”策略。你也可以选择抛出异常,或者定义一个专门的“默认处理者”来处理所有未被处理的请求。
注意:这个基础版本有一个潜在问题:如果某个处理者“忘记”在无法处理时调用父类的传递方法(
AbstractLeaveHandler::handleRequest(days)),那么链就会在此处断掉,请求无法继续传递。这是实现时需要严格检查的点。
3. 职责链模式的经典与进阶应用场景
光看请假审批的例子可能觉得有点“玩具化”,下面我分享几个在真实项目中,职责链模式大放异彩的场景,你会看到它的威力。
3.1 场景一:网络请求处理管道(中间件)
这是Web服务器框架(如Nginx模块、某些C++ HTTP库)中的典型应用。一个HTTP请求进来,需要经过一系列中间件(Middleware)的处理:身份验证、日志记录、请求体解析、路由匹配、业务逻辑处理、响应格式化等。
// 简化的中间件处理者接口 class Middleware { public: virtual ~Middleware() = default; virtual void setNext(std::shared_ptr<Middleware> next) = 0; virtual bool process(HttpRequest& req, HttpResponse& resp) = 0; // 返回是否继续 }; class AuthMiddleware : public AbstractMiddleware { /* 检查Token/Cookie */ }; class LoggingMiddleware : public AbstractMiddleware { /* 记录请求日志 */ }; class BodyParserMiddleware : public AbstractMiddleware { /* 解析JSON/Form数据 */ }; class RouterMiddleware : public AbstractMiddleware { /* 查找对应的Controller */ }; // 在服务器启动时组装链 auto chain = std::make_shared<AuthMiddleware>(); chain->setNext(std::make_shared<LoggingMiddleware>()) ->setNext(std::make_shared<BodyParserMiddleware>()) ->setNext(std::make_shared<RouterMiddleware>()); // 每个请求到来时 HttpRequest req = getIncomingRequest(); HttpResponse resp; chain->process(req, resp); // 请求沿着管道流动优势:每个中间件功能单一,易于测试和复用。你可以像搭积木一样随意调整中间件的顺序(比如把日志放在最前或最后),或者轻松插入新的中间件(如限流、缓存),而无需修改其他中间件的代码。
3.2 场景二:游戏中的输入事件处理
在游戏开发中,玩家的一个按键或鼠标点击事件,可能需要被多个系统消费。比如,按“E”键,优先级顺序可能是:首先判断是否在UI界面(打开背包),其次判断是否与NPC对话,最后才是玩家角色的通用交互(如拾取物品)。
class InputEventHandler { public: virtual bool handle(const InputEvent& event) = 0; // 返回true表示已消费事件 virtual void setNext(std::shared_ptr<InputEventHandler> next) = 0; }; class UIEventHandler : public AbstractInputEventHandler { bool handle(const InputEvent& event) override { if (isCursorOverUI()) { // 处理UI交互... return true; // 事件已消费,停止传递 } return AbstractInputEventHandler::handle(event); // 传递给下一个 } }; class DialogueEventHandler : public AbstractInputEventHandler { bool handle(const InputEvent& event) override { if (isPlayerInDialogueRange()) { // 触发对话... return true; } return AbstractInputEventHandler::handle(event); } }; class PlayerActionHandler : public AbstractInputEventHandler { bool handle(const InputEvent& event) override { // 默认的玩家动作(行走、攻击、拾取) // ... return true; // 通常作为链的末端,总是消费事件 } };优势:实现了输入事件的“冒泡”处理。高优先级的系统(如UI)先拿到事件,如果它处理了,事件就不会再传到后面的游戏逻辑,避免了冲突。这比在所有系统里都写一遍“如果UI激活则忽略输入”要清晰得多。
3.3 场景三:日志系统的多级输出
一个成熟的日志系统需要支持同时输出到多个目的地(控制台、文件、网络、数据库),并且每个目的地可能有不同的日志级别过滤。
class LogSink { public: virtual void setNext(std::shared_ptr<LogSink> next) = 0; virtual void write(const LogMessage& msg) = 0; }; class ConsoleSink : public AbstractLogSink { void write(const LogMessage& msg) override { if (msg.level >= LogLevel::INFO) { // 控制台只输出INFO及以上 std::cout << "[CONSOLE] " << msg.text << std::endl; } AbstractLogSink::write(msg); // 无论是否输出,都传递给下一个Sink } }; class FileSink : public AbstractLogSink { void write(const LogMessage& msg) override { if (msg.level >= LogLevel::DEBUG) { // 文件记录更详细的DEBUG信息 logFile_ << "[FILE] " << msg.text << std::endl; } AbstractLogSink::write(msg); } }; class NetworkSink : public AbstractLogSink { void write(const LogMessage& msg) override { if (msg.level >= LogLevel::ERROR) { // 只有ERROR才发往网络监控 sendToNetwork(msg); } AbstractLogSink::write(msg); } }; // 使用时,一条日志会依次经过控制台、文件、网络三个处理者,每个根据自己的规则决定是否记录。优势:输出策略(目的地和级别)完全解耦。你可以动态地添加或移除Sink(比如在检测到磁盘满时关闭FileSink),而日志产生模块对此一无所知。
3.4 场景四:数据验证与清洗管道
在处理用户提交的表单数据或外部API数据时,往往需要经过一系列验证和清洗步骤:非空检查、格式校验(邮箱、手机号)、去重、敏感词过滤、数据格式化等。
class DataFilter { public: virtual std::optional<std::string> filter(std::string& data) = 0; // 返回错误信息,空表示通过 virtual void setNext(std::shared_ptr<DataFilter> next) = 0; }; class NotEmptyFilter : public AbstractDataFilter { std::optional<std::string> filter(std::string& data) override { if (data.empty()) return "数据不能为空"; return AbstractDataFilter::filter(data); } }; class EmailFormatFilter : public AbstractDataFilter { std::optional<std::string> filter(std::string& data) override { if (!isValidEmail(data)) return "邮箱格式不正确"; return AbstractDataFilter::filter(data); } }; class SensitiveWordFilter : public AbstractDataFilter { std::optional<std::string> filter(std::string& data) override { if (containsSensitiveWords(data)) { data = maskSensitiveWords(data); // 注意:这里可以修改数据 // 可以记录日志,但不一定算错误 } return AbstractDataFilter::filter(data); } }; // 数据依次通过各个过滤器,任何一个过滤器返回错误,整个链就提前终止并返回错误。优势:验证规则模块化,易于维护和扩展。新增一种验证(如手机号国际区号校验),只需新增一个Filter类并插入链中即可。规则执行的顺序也一目了然。
4. C++实现中的高级技巧与避坑指南
掌握了基础和应用场景,我们来看看在C++里实现职责链时,有哪些可以优化和需要注意的“坑”。
4.1 实现技巧:让链的构建更流畅
上面的例子中,我们通过setNext一个个地链接节点。可以借鉴“建造者模式”的思想,实现一个流畅接口(Fluent Interface),让链的构建像写句子一样自然。
class Handler { public: virtual Handler* setNext(Handler* next) { next_ = next; return next; // 关键:返回下一个处理者,以便连续调用 } // ... 其他成员 private: Handler* next_; }; // 使用流畅接口构建链 Handler* chain = new ConcreteHandlerA(); chain->setNext(new ConcreteHandlerB()) ->setNext(new ConcreteHandlerC()); // 现在chain是A,A后面连着B,B后面连着C这是很多经典示例(包括你提供的Refactoring.Guru的代码)采用的方式。它简洁,但需要注意内存管理,因为返回的是原始指针。
4.2 内存管理:智能指针的正确姿势
在现代C++中,强烈推荐使用智能指针。但用shared_ptr时,直接返回shared_ptr<Handler>才能实现流畅接口。
class Handler : public std::enable_shared_from_this<Handler> { public: virtual std::shared_ptr<Handler> setNext(std::shared_ptr<Handler> next) { next_ = next; return next; // 返回下一个处理者的shared_ptr } virtual void handle(Request& req) { if (next_) { next_->handle(req); } } protected: std::shared_ptr<Handler> next_; }; auto handlerA = std::make_shared<ConcreteHandlerA>(); auto handlerB = std::make_shared<ConcreteHandlerB>(); auto handlerC = std::make_shared<ConcreteHandlerC>(); // 流畅构建,且所有权清晰 handlerA->setNext(handlerB)->setNext(handlerC);如果链是单向的,且所有权明确从头部流向尾部,使用unique_ptr也是可以的,但setNext需要接收unique_ptr参数并转移所有权,流畅接口的实现会稍复杂。
4.3 性能考量:避免不必要的传递
职责链的一个潜在缺点是,如果链很长,且请求最终才被处理或无人处理,它需要遍历整个链,可能带来性能损耗。
- 短路优化:这是最直接的优化。一旦某个处理者决定处理请求,就立即返回,不再向后传递。这要求
handle方法有明确的返回值来指示“已处理”。上面的很多例子都隐含了这个逻辑。 - 缓存与索引:对于某些特定类型的请求,如果其处理者在链中的位置相对固定,可以考虑建立简单的映射(如
std::unordered_map<RequestType, Handler*>),直接跳转到可能处理它的节点开始,而不是每次都从头部开始。但这会引入额外的复杂度,破坏了一些解耦性。 - 异步职责链:在处理IO密集型或需要等待的任务时,可以让每个处理者异步操作,并通过回调或Promise/Future将处理结果传递下去。这能提高系统的并发能力,但实现复杂度陡增。
4.4 常见问题与排查实录
在实际使用中,我踩过不少坑,这里总结几个最常见的:
链断裂了,请求石沉大海
- 症状:请求发送后,没有任何处理者响应,也没有到达末端提示。
- 排查:这是最经典的错误。百分之九十的原因,是某个具体处理者在“自己无法处理”的分支里,忘记调用基类的
handle方法(或next_->handle)来传递请求。仔细检查每个ConcreteHandler::handleRequest中的else分支或return语句前。 - 预防:在抽象基类中提供一个安全的默认传递实现,并强制要求子类在无法处理时调用它。代码审查时重点看这里。
内存泄漏或重复释放
- 症状:程序运行一段时间后内存增长,或在退出时崩溃。
- 排查:如果使用原始指针,检查链的构建和销毁过程。谁负责
delete?如果链是全局或长期存在的,在程序退出时是否需要手动遍历销毁?如果处理者还在链中就被局部delete了,会导致悬空指针。 - 解决:无脑使用智能指针(
shared_ptr或unique_ptr)。对于明确的单向所有权链,unique_ptr是首选。如果处理者需要被多处引用(比如同时被链和某个管理器持有),再用shared_ptr。
循环引用导致内存泄漏(使用shared_ptr时)
- 症状:即使使用了
shared_ptr,对象依然不被释放。 - 排查:检查链中是否有形成“环”的可能?比如,A的
next_指向B,而B的next_又指回了A(或者间接指回)。shared_ptr的循环引用会导致引用计数永远不为0。 - 解决:职责链通常是单向的,一般不会形成环。但如果设计复杂链(如树状、图状),需要考虑将某些链接改为
weak_ptr来打破循环。
- 症状:即使使用了
处理顺序不符合预期
- 症状:请求被错误的处理者处理了,或者处理了多次。
- 排查:首先检查链的组装顺序是否正确。其次,检查每个处理者的判断条件是否有重叠或漏洞。例如,经理的审批条件是
days <= 7,组长的条件是days <= 3,那么请4天假,组长处理不了会传递给经理,这是对的。但如果经理的条件误写为days >= 4,那么请3天假也会被经理处理,这就乱了。 - 预防:在单元测试中,针对边界条件(如例子中的3天、7天)进行充分测试。明确每个处理者的职责范围,最好写成互斥的区间。
5. 与其他设计模式的关联与选择
设计模式很少孤立使用。职责链常与其他模式搭配,解决更复杂的问题。
- 与组合模式(Composite)结合:这可以形成“树形职责链”。一个复合处理者(Composite Handler)可以包含多个子处理者,它接收到请求后,可以传递给所有子处理者(广播),或者按特定策略选择一个子处理者传递。这在实现复杂的UI事件分发或组织架构审批(如会签)时很有用。
- 与命令模式(Command)结合:职责链中的“请求”可以是一个命令对象。这样,处理者不仅决定是否处理,还可以决定如何执行、撤销命令等。增加了请求的灵活性和可扩展性。
- 与模板方法模式(Template Method)结合:我们的
AbstractHandler基类已经用到了模板方法的思想。它定义了处理请求的骨架(“先尝试自己处理,不行就传递”),而将具体的处理逻辑延迟到子类中实现。
什么时候不该用职责链?当请求有且只有一个明确的处理者时,直接用简单的条件判断或查找表(如std::map)会更高效、更直接。职责链引入了动态查找的开销,在性能极度敏感或处理逻辑极其稳定的场景下,可能是过度设计。
6. 实战:构建一个可配置的日志处理框架
最后,我们综合运用以上知识,设计一个简易但实用的日志处理框架。它支持动态添加/移除日志输出器(Sink),每个Sink可以设置独立的日志级别和格式。
#include <iostream> #include <memory> #include <vector> #include <string> #include <sstream> #include <chrono> // 日志级别 enum class LogLevel { DEBUG, INFO, WARN, ERROR }; // 日志消息 struct LogMessage { LogLevel level; std::string text; std::chrono::system_clock::time_point timestamp; std::string source; // 可选,日志来源 std::string toString() const { std::ostringstream oss; auto t = std::chrono::system_clock::to_time_t(timestamp); oss << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S"); return "[" + oss.str() + "] [" + levelToString(level) + "] " + text; } private: static std::string levelToString(LogLevel l) { switch(l) { case LogLevel::DEBUG: return "DEBUG"; case LogLevel::INFO: return "INFO"; case LogLevel::WARN: return "WARN"; case LogLevel::ERROR: return "ERROR"; default: return "UNKNOWN"; } } }; // 日志处理器接口 class LogSink { public: virtual ~LogSink() = default; virtual void setNext(std::shared_ptr<LogSink> next) = 0; virtual void write(const LogMessage& msg) = 0; virtual LogLevel getLevel() const = 0; }; // 抽象基类:实现链式传递和级别过滤 class AbstractLogSink : public LogSink { protected: std::shared_ptr<LogSink> next_; LogLevel minLevel_; // 该Sink处理的最低级别 public: explicit AbstractLogSink(LogLevel minLevel) : next_(nullptr), minLevel_(minLevel) {} void setNext(std::shared_ptr<LogSink> next) override { next_ = next; } void write(const LogMessage& msg) override { // 1. 判断级别:只有达到最低级别才处理 if (msg.level >= minLevel_) { // 2. 执行具体的输出逻辑(由子类实现) this->writeImpl(msg); } // 3. 无论是否处理,都传递给下一个Sink(保证日志能到达所有符合条件的Sink) if (next_) { next_->write(msg); } } LogLevel getLevel() const override { return minLevel_; } protected: virtual void writeImpl(const LogMessage& msg) = 0; // 子类实现具体输出 }; // 具体Sink:输出到控制台 class ConsoleSink : public AbstractLogSink { public: ConsoleSink(LogLevel level) : AbstractLogSink(level) {} protected: void writeImpl(const LogMessage& msg) override { std::cout << "[Console] " << msg.toString() << std::endl; } }; // 具体Sink:输出到文件 class FileSink : public AbstractLogSink { std::ofstream file_; public: FileSink(const std::string& filename, LogLevel level) : AbstractLogSink(level), file_(filename, std::ios::app) { if (!file_.is_open()) { throw std::runtime_error("无法打开日志文件: " + filename); } } protected: void writeImpl(const LogMessage& msg) override { file_ << "[File] " << msg.toString() << std::endl; } }; // 具体Sink:输出到网络(模拟) class NetworkSink : public AbstractLogSink { std::string remoteAddr_; public: NetworkSink(const std::string& addr, LogLevel level) : AbstractLogSink(level), remoteAddr_(addr) {} protected: void writeImpl(const LogMessage& msg) override { // 模拟网络发送,实际项目中可能用socket std::cout << "[Network->" << remoteAddr_ << "] " << msg.toString() << std::endl; } }; // 日志器客户端,封装链的创建和使用 class Logger { std::shared_ptr<LogSink> sinkChain_; public: Logger() : sinkChain_(nullptr) {} // 添加一个Sink到链的末尾 void addSink(std::shared_ptr<LogSink> sink) { if (!sinkChain_) { sinkChain_ = sink; } else { // 找到链的末尾并添加 auto current = sinkChain_; while (auto next = std::dynamic_pointer_cast<AbstractLogSink>(current)->getNextRawPtr()) { // 需要额外方法获取next current = next; } current->setNext(sink); } } // 便捷方法:直接构建一个常用链 static std::shared_ptr<Logger> createDefault() { auto logger = std::make_shared<Logger>(); logger->addSink(std::make_shared<ConsoleSink>(LogLevel::INFO)); // 控制台只输出INFO及以上 logger->addSink(std::make_shared<FileSink>("app.log", LogLevel::DEBUG)); // 文件记录所有DEBUG及以上 logger->addSink(std::make_shared<NetworkSink>("192.168.1.100:514", LogLevel::ERROR)); // 网络只发ERROR return logger; } void log(LogLevel level, const std::string& text) { LogMessage msg{level, text, std::chrono::system_clock::now(), "Main"}; if (sinkChain_) { sinkChain_->write(msg); } } }; // 使用示例 int main() { auto logger = Logger::createDefault(); logger->log(LogLevel::DEBUG, "这是一条调试信息,只有文件能看到。"); logger->log(LogLevel::INFO, "程序启动成功。"); // 控制台和文件能看到 logger->log(LogLevel::WARN, "磁盘空间不足。"); // 控制台和文件能看到 logger->log(LogLevel::ERROR, "数据库连接失败!"); // 控制台、文件、网络都能看到 // 动态添加一个新的Sink logger->addSink(std::make_shared<ConsoleSink>(LogLevel::WARN)); // 再添加一个只输出WARN以上的控制台Sink std::cout << "\n--- 动态添加Sink后 ---\n"; logger->log(LogLevel::INFO, "这条信息只会被第一个ConsoleSink和FileSink处理。"); return 0; }这个框架展示了职责链模式的强大之处:
- 开闭原则:要新增一个输出目的地(比如数据库Sink),只需新建一个类继承
AbstractLogSink,实现writeImpl,然后addSink即可。无需修改任何现有代码。 - 单一职责:每个Sink只关心自己的输出方式和级别过滤。
- 动态配置:日志链可以在运行时动态改变,比如根据配置文件的开关决定是否启用网络Sink,或者根据日志级别动态调整输出目标。
最后的个人体会:职责链模式是一个“润物细无声”的模式。当你发现代码里有一长串if-else if来判断“谁该处理这个请求”时,就是它登场的最佳时机。在C++中实现,关键是要处理好对象生命周期(多用智能指针),设计好清晰的处理接口和传递逻辑。它可能不会让你的程序性能飞升,但会极大提升代码的可读性、可维护性和可扩展性,这在长期维护和团队协作中价值连城。下次面试被问到设计模式,不妨就拿这个日志框架的例子来阐述,既有理论又有实践,绝对能加分。