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_ptrLeaveHandler next) 0; // 处理请假请求 virtual void handleRequest(int days) 0; }; // 2. 抽象处理者实现链的传递逻辑 class AbstractLeaveHandler : public LeaveHandler { protected: std::shared_ptrLeaveHandler nextHandler_; // 指向下一个处理者的智能指针 public: AbstractLeaveHandler() : nextHandler_(nullptr) {} void setNext(std::shared_ptrLeaveHandler 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_sharedGroupLeaderHandler(); auto manager std::make_sharedManagerHandler(); auto director std::make_sharedDirectorHandler(); // 组装职责链组长 - 经理 - 总监 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_ptrMiddleware 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_sharedAuthMiddleware(); chain-setNext(std::make_sharedLoggingMiddleware()) -setNext(std::make_sharedBodyParserMiddleware()) -setNext(std::make_sharedRouterMiddleware()); // 每个请求到来时 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_ptrInputEventHandler 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_ptrLogSink 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::optionalstd::string filter(std::string data) 0; // 返回错误信息空表示通过 virtual void setNext(std::shared_ptrDataFilter next) 0; }; class NotEmptyFilter : public AbstractDataFilter { std::optionalstd::string filter(std::string data) override { if (data.empty()) return 数据不能为空; return AbstractDataFilter::filter(data); } }; class EmailFormatFilter : public AbstractDataFilter { std::optionalstd::string filter(std::string data) override { if (!isValidEmail(data)) return 邮箱格式不正确; return AbstractDataFilter::filter(data); } }; class SensitiveWordFilter : public AbstractDataFilter { std::optionalstd::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是AA后面连着BB后面连着C这是很多经典示例包括你提供的Refactoring.Guru的代码采用的方式。它简洁但需要注意内存管理因为返回的是原始指针。4.2 内存管理智能指针的正确姿势在现代C中强烈推荐使用智能指针。但用shared_ptr时直接返回shared_ptrHandler才能实现流畅接口。class Handler : public std::enable_shared_from_thisHandler { public: virtual std::shared_ptrHandler setNext(std::shared_ptrHandler next) { next_ next; return next; // 返回下一个处理者的shared_ptr } virtual void handle(Request req) { if (next_) { next_-handle(req); } } protected: std::shared_ptrHandler next_; }; auto handlerA std::make_sharedConcreteHandlerA(); auto handlerB std::make_sharedConcreteHandlerB(); auto handlerC std::make_sharedConcreteHandlerC(); // 流畅构建且所有权清晰 handlerA-setNext(handlerB)-setNext(handlerC);如果链是单向的且所有权明确从头部流向尾部使用unique_ptr也是可以的但setNext需要接收unique_ptr参数并转移所有权流畅接口的实现会稍复杂。4.3 性能考量避免不必要的传递职责链的一个潜在缺点是如果链很长且请求最终才被处理或无人处理它需要遍历整个链可能带来性能损耗。短路优化这是最直接的优化。一旦某个处理者决定处理请求就立即返回不再向后传递。这要求handle方法有明确的返回值来指示“已处理”。上面的很多例子都隐含了这个逻辑。缓存与索引对于某些特定类型的请求如果其处理者在链中的位置相对固定可以考虑建立简单的映射如std::unordered_mapRequestType, 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_ptrLogSink next) 0; virtual void write(const LogMessage msg) 0; virtual LogLevel getLevel() const 0; }; // 抽象基类实现链式传递和级别过滤 class AbstractLogSink : public LogSink { protected: std::shared_ptrLogSink next_; LogLevel minLevel_; // 该Sink处理的最低级别 public: explicit AbstractLogSink(LogLevel minLevel) : next_(nullptr), minLevel_(minLevel) {} void setNext(std::shared_ptrLogSink 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_ptrLogSink sinkChain_; public: Logger() : sinkChain_(nullptr) {} // 添加一个Sink到链的末尾 void addSink(std::shared_ptrLogSink sink) { if (!sinkChain_) { sinkChain_ sink; } else { // 找到链的末尾并添加 auto current sinkChain_; while (auto next std::dynamic_pointer_castAbstractLogSink(current)-getNextRawPtr()) { // 需要额外方法获取next current next; } current-setNext(sink); } } // 便捷方法直接构建一个常用链 static std::shared_ptrLogger createDefault() { auto logger std::make_sharedLogger(); logger-addSink(std::make_sharedConsoleSink(LogLevel::INFO)); // 控制台只输出INFO及以上 logger-addSink(std::make_sharedFileSink(app.log, LogLevel::DEBUG)); // 文件记录所有DEBUG及以上 logger-addSink(std::make_sharedNetworkSink(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_sharedConsoleSink(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中实现关键是要处理好对象生命周期多用智能指针设计好清晰的处理接口和传递逻辑。它可能不会让你的程序性能飞升但会极大提升代码的可读性、可维护性和可扩展性这在长期维护和团队协作中价值连城。下次面试被问到设计模式不妨就拿这个日志框架的例子来阐述既有理论又有实践绝对能加分。