资讯动态

C++职责链模式四种工程变体实战指南

发布时间:2026/10/17 11:09:25 来源:尧图企业网站定制
接手过一个项目核心业务是处理来自不同渠道的用户请求。早期代码里全是嵌套的 if-else每次增加新的请求来源都要改动那一段越来越臃肿的判断逻辑。同事开玩笑说这代码就像千层饼每一层都有它存在的理由但没人敢动它。后来我翻资料时重新审视了职责链模式发现它在 C 里能玩的花样远不止教科书上那种沿着链表往下传的写法。这篇文章就把我实际用过的几种职责链变体连同踩过的坑一起整理出来。1. 经典职责链在 C 里的根基从多条件判定到处理器串联1.1 职责链模式解决的问题与教科书实现职责链模式属于行为型设计模式核心思想是把一个请求的发送者与接收者解耦让多个对象都有机会处理这个请求并沿着链传递下去直到有对象处理为止。它解决的核心痛点就是开头提到的场景系统中存在大量基于类型、状态、来源或上下文进行差异化处理的逻辑而写成一连串条件分支会让代码难以维护。教科书版本通常长这样class Handler { public: virtual ~Handler() default; void setNext(std::shared_ptrHandler next) { next_ std::move(next); } virtual void handle(const Request req) { if (canHandle(req)) { process(req); } else if (next_) { next_-handle(req); } } protected: virtual bool canHandle(const Request req) 0; virtual void process(const Request req) 0; private: std::shared_ptrHandler next_; };这种实现把处理器抽象成基类子类各自实现canHandle和process。链条的查找逻辑被封装在handle方法里先判断自己能不能处理不能就交给下一个节点。当时我照着这个模式重构了那堆 if-else确实清爽了不少。每个请求来源变成独立处理器类加新来源就继承基类写两个方法在主流程里把处理器串联起来。但用了一阵子问题就来了。1.2 传统实现的两个显而易见的毛病第一个毛病是装配链条的过程很脆弱。每次都要手动创建对象、调用setNext万一顺序传错整个链的逻辑就变了。更头疼的是如果某个处理器在运行过程中想让链条动态调整比如临时禁用一个来源或者按请求的不同走不同分支传统链表结构改起来非常费劲。第二个毛病是链上的节点职责太重。每个节点为了确定自己是否处理要在canHandle里解析请求这导致处理器之间其实隐藏着隐式耦合——你处理哪些请求我处理哪些请求边界互相依赖。改了一个节点的判定条件很可能让相邻节点的处理逻辑落空或重复。当然这些不全是模式的错更像是我在工程实践中的粗糙用法暴露的问题。但既然有了痛点自然就有了各种顺手改良的动力。后面这四种变体我都是在不同项目里针对不同痛点逐步摸索出来的。2. 变体一函数式职责链——用std::function取代硬编码继承体系2.1 思路转变处理器不一定要是类传统职责链的第一性问题是把处理器设计成了抽象基类逼着每个处理逻辑都套一层类的壳。但 C 里处理一段逻辑更自然的方式是函数。于是变体一的思路很简单把处理器改成std::functionbool(const Request)链条就是一组函数的集合。这样做的收益很直接不需要为每一种处理逻辑新建类Lambda 表达式直接就能作为处理器加入链中识别简单逻辑时连函数都不用拆。它还顺手解决了处理器粒度问题——传统模式下逻辑再短也必须建类函数式变体允许你按内聚度自由控制每个处理器的规模。2.2 函数式职责链的最小实现#include functional #include vector #include iostream struct Request { std::string source; int priority; }; class FunctionalChain { public: using Processor std::functionbool(const Request); // 添加处理器返回引用以便连续调用 FunctionalChain then(Processor proc) { processors_.push_back(std::move(proc)); return *this; } // 逐个执行处理器直到某个返回 true 表示已处理 bool handle(const Request req) { for (auto proc : processors_) { if (proc(req)) { return true; } } return false; // 没有任何处理器处理 } private: std::vectorProcessor processors_; }; // 使用示例 int main() { FunctionalChain chain; chain .then([](const Request r) { if (r.source mobile) { std::cout handle mobile request std::endl; return true; } return false; }) .then([](const Request r) { if (r.priority 5) { std::cout handle high priority request std::endl; return true; } return false; }) .then([](const Request r) { std::cout catch-all: handle everything else std::endl; return true; // 兜底处理器无论如何处理 }); chain.handle({mobile, 3}); chain.handle({web, 10}); }这个实现的then返回FunctionalChain让调用点能连续挂处理器。handle只关心返回布尔值true 代表已处理停止后续节点false 则继续。这种写法比基类版本直白很多。2.3 函数式变体的边界与使用建议这种变体对配置式逻辑尤其友好。比如不同客户要求的处理顺序不同直接运行时往 vector 里插删 Lambda 就能动态调整完全不需要改动处理器类。它还天然兼容捕获上下文的 Lambda比如多个处理器共享一个状态对象捕获列表直接引用即可。不过它也有自己的代价丢失了类型信息。每个处理器只是个黑盒无法直观地看出它的身份。还有如果你需要长链递归调用、处理器内部继续下分一条子链比如树状职责链函数式写法会变得绕。我的规则很简单处理器内聚逻辑简单优先选函数式如果处理器自身还有复杂的处理策略那么类方案更适合。3. 变体二观察者型职责链——请求在链上广播而非中断3.1 场景驱动从找一个处理器到所有处理器都看看教科书职责链有个默认假设请求最终只被一个处理器消费。但现实中常遇到另一种需求一个事件必须让所有环节都过目一遍。最典型的是日志链路一条日志要依次经过格式化、过滤、写入多个介质任何一个环节都不能因为前面处理了就跳过。再比如网络报文要跨过多个中间件每个中间件都要读改写报文体。这种全链广播需求与其叫职责链不如说它是观察者模式的一种有序变体。它和观察者的区别在于观察者通常没有次序概念而这条广播链对顺序有明确要求——比如确保格式转换在压缩之前。3.2 支持广播语义的职责链实现#include vector #include functional #include iostream class BroadcastChain { public: using Processor std::functionvoid(Request); BroadcastChain then(Processor proc) { processors_.push_back(std::move(proc)); return *this; } // 注意不中断每个处理器都会执行 void handle(Request req) { for (auto proc : processors_) { proc(req); // 顺序执行 } } private: std::vectorProcessor processors_; }; // 使用示例日志处理链路 int main() { BroadcastChain logPipeline; logPipeline .then([](Request r) { r.message [formatted] r.message; // 步骤1格式化 }) .then([](Request r) { if (r.message.find(DEBUG) ! std::string::npos) { std::cout debug message, just record std::endl; } // 步骤2过滤 }) .then([](Request r) { std::cout final log: r.message std::endl; // 步骤3输出 }); Request req{something, 1}; // 注意这里需要在 Request 中加入 message 字段 req.message hello; logPipeline.handle(req); }核心差异就在handle实现for循环里没有return true的退出条件每个处理器都会按顺序执行。处理器可以传递中间状态比如格式化后的req.message被下一个处理器读取这让多个处理步骤形成了真正的流水线。3.3 变体二的变体会话带中断的广播链在某些场景里广播和中断可以混用。我做过一个请求审计系统要求是所有节点都必须记录审计日志但一旦某个节点判定请求非法后续节点就不再处理业务逻辑仅保留日志记录。这种混合链需要一个可选的终止标记// 处理器返回 true 表示继续false 表示中断后续业务处理 class InterruptibleBroadcastChain { public: using Processor std::functionbool(Request); void then(Processor proc) { processors_.push_back(std::move(proc)); } bool handle(Request req) { bool shouldContinue true; for (auto proc : processors_) { // 先让处理器执行再看它的意见 bool result proc(req); if (!result) { shouldContinue false; // 某节点要求中断 } // 注意这里没有 break因为日志节点已经记完账了 // 如果要区分审计和业务可以在 Processor 上再加个类别标记 } return shouldContinue; } private: std::vectorProcessor processors_; };这种写法保持了全节点执行的特性同时通过返回值传递是否继续的信号。虽然应用面窄但在审计、计费这类既要全程监测又要临时阻断的流程里非常好用。4. 变体三可装配的职责链——用ChainBuilder让链条本身可配置4.1 问题升级运行时动态组装链函数式链解决了处理器组件化的痛点但在真正企业级系统里还有一个更棘手的问题整条链的装配顺序本身需要可配置。比如运营人员通过后台界面切换不同的处理流程或者 A/B 实验希望一半请求走轻量链、一半请求走完整链。链的形态不能写死在代码里得做成运行时可加载的配置。这就是可装配职责链的动机。它把链拆成了两段处理器注册表保存所有可用的处理器和链装配器按配置生成具体的链对象。4.2 ChainBuilder 的 C17 实现#include map #include memory #include string #include vector #include functional #include iostream class ChainBuilder { public: // 注册处理器给它一个名字方便配置引用 using Processor std::functionbool(const Request); templatetypename F ChainBuilder registerProcessor(const std::string name, F func) { registry_[name] std::forwardF(func); return *this; } // 根据名称列表构建链 std::vectorProcessor build(const std::vectorstd::string configNames) { std::vectorProcessor result; for (const auto name : configNames) { auto it registry_.find(name); if (it registry_.end()) { throw std::runtime_error(unknown processor: name); } result.push_back(it-second); } return result; } const std::mapstd::string, Processor registry() const { return registry_; } private: std::mapstd::string, Processor registry_; }; // 模拟配置来源实际项目里可能是 JSON / 数据库表 int main() { ChainBuilder builder; builder .registerProcessor(mobile, [](const Request r) { std::cout mobile handler std::endl; return r.source mobile; }) .registerProcessor(high_pri, [](const Request r) { std::cout high priority handler std::endl; return r.priority 5; }) .registerProcessor(default, [](const Request r) { std::cout default handler std::endl; return true; }); // 模拟某客户要求先处理 mobile再走 default auto chain1 builder.build({mobile, default}); // 模拟另一客户要求只走 high_pri auto chain2 builder.build({high_pri}); Request req1{mobile, 1}; for (auto proc : chain1) { if (proc(req1)) break; } }这个ChainBuilder是典型的配置驱动装配思路。registerProcessor用字符串索引处理器build依据配置名的顺序生成链。你甚至可以封装一个createChain(jsonString)方法把配置解析逻辑放在内部让上层业务完全不知道处理器类的存在。4.3 动态改链的实践经验在真实项目里这种装配器最大价值在于热更新。我做过一个网关系统运营人员会不定时调整中间件的启停顺序起初每次都要重启进程。引入 ChainBuilder 后配置变更只触发一次build然后通过 atomic 指针替换旧链对象支撑并发请求的代码完全无感。这里要特别提醒一个坑动态构建的链对象生命周期管理。如果请求正在执行某个旧链线程切换后你又替换了链对象那么旧链的处理器对象可能被析构。我当时的做法是把链存放在std::shared_ptr里用std::atomic_load获取当前链确保没有请求持有旧链引用后才销毁。 注意使用 atomic 指针替换链对象时处理器内部如果有状态对象也要考虑线程安全性。纯函数的处理器不会有这个问题但有状态处理器需要你显式加锁或者使用线程局部存储。5. 变体四编译期职责链——模板元编程实现的零开销链5.1 思路链在编译期就确定下来前面几个变体虽然好用但都有一个共同特点处理器保存在std::vector里handle循环有运行时开销函数调用、虚表或 std::function 的间接调用。在某些对性能极度敏感的场景每请求纳秒级别这个开销是不可接受的。此时编译期职责链登场。它的核心思路是如果处理器类型在编译期就已知并且链路不会运行时变动那么完全可以用模板把链拍平成分层函数调用编译器在优化后甚至能内联掉所有中间层达到接近手写 if-else 的性能。5.2 用 if constexpr 实现编译期责任链#include type_traits #include iostream // 前置声明返回 bool 表示处理完毕 templatetypename... Processors class CompileTimeChain; // 特化空链返回 false 表示未处理 template class CompileTimeChain { public: bool handle(const Request req) { return false; } }; // 递归情况处理第一个处理器若不能处理则交给剩余处理器 templatetypename First, typename... Rest class CompileTimeChainFirst, Rest... { public: bool handle(const Request req) { // 尝试第一个 if (First{}(req)) { return true; } // 交给剩余部分 return CompileTimeChainRest...{}.handle(req); } }; // 处理器示例 struct MobileHandler { bool operator()(const Request req) const { std::cout mobile handler (compile-time) std::endl; return req.source mobile; } }; struct HighPriorityHandler { bool operator()(const Request req) const { std::cout high priority handler (compile-time) std::endl; return req.priority 5; } }; struct DefaultHandler { bool operator()(const Request req) const { std::cout default handler (compile-time) std::endl; return true; } }; // 使用示例 int main() { using MyChain CompileTimeChainMobileHandler, HighPriorityHandler, DefaultHandler; MyChain chain; Request req{web, 10}; chain.handle(req); // 会依次尝试 Mobile - HighPriority最后命中 Default }可以看到编译器会为CompileTimeChainMobileHandler, HighPriorityHandler, DefaultHandler生成一层层嵌套的handle调用先调MobileHandler::operator()返回 false 再调下一个以此类推。如果处理器逻辑简单整个链条在-O2下会被完全内联变成一段顺序执行的扁平代码几乎零额外开销。5.3 编译期责任链的适用边界这种变体漂亮但代价也很明显不可运行时配置。你不能根据用户配置改变链的顺序处理器类型必须编译期确定。它适合的场景是那些链路固定、命中条件简单、性能优先的内部逻辑比如游戏引擎里的事件到响应分派、驱动里的命令路由表。它还能配合if constexpr做条件编译。比如某些平台不需要某个处理器直接传入空类型让特化版本跳过// 如果用 if constexpr 包装根据编译期标志决定是否包含某个节点 templatebool NeedMobile using ConditionalChain CompileTimeChain std::conditional_tNeedMobile, MobileHandler, NopHandler, HighPriorityHandler, DefaultHandler ;这里NopHandler永远返回 false相当于占位空节点让结构保持统一。这算是我觉得编译期链最优雅的玩法类型层面的条件分支代替运行时的布尔判断。6. 四种变体的对比与选型遇到真实场景怎么选6.1 核心维度对比维度经典职责链函数式链广播链可装配链编译期链处理器表现形式类/虚函数std::function / Lambdastd::functionstd::function 名字注册类型/仿函数链是否运行时可变弱改指针容易vector 增删容易容易配置驱动不可变执行模式找到即停找到即停全部执行按需求实现找到即停性能中虚函数开销中function 开销中中最优可内联代码可读性中高链式调用直观高中高低模板元编程适用场景组件化处理器灵活轻量级链路流水线处理配置驱动的中间件固定热路径这个表基本能覆盖我遇到过的绝大多数选择场景。简单说处理器内部逻辑复杂就选经典类方案链路简单灵活就选函数式需要广播式处理就用广播链系统需要配置热更新就上 ChainBuilder极端追求性能且链路固定就上编译期链。6.2 我踩过的坑和一些实战建议第一个坑跟处理器顺序耦合有关。做网关中间件时我把鉴权放在限流前后来产品说限流应该先做——因为未鉴权的请求也可能触发限流保护。结果只改一行配置顺序但两个处理器内部假定的前置条件全部崩了。所以无论哪种链条处理器之间最好不要依赖隐含的先后状态非得依赖时在处理器代码里用注释写清楚此节点先于 XXX 节点执行。第二个坑是链的最终兜底设计。函数式链容易忘记最后加一个总得有人处理的兜底节点。我见过一个支付回调处理链因为某天新增一种回调来源没配处理器整个链路直接返回 false上层把回调当失败处理了。后来我统一约定所有链结构从设计上必须有一个catchAll节点兜底逻辑至少要记录日志返回 true宁可默默处理不可让请求悬空。第三个是生命周期和共享状态。函数式链的处理器如果捕获了外部状态引用链传递过程中千万注意这些状态的线程安全性。尤其是可装配链处理器实例经常是共享的内部一旦有 mutable 成员变量并发风险瞬间拉满。我的经验是能写成纯函数的绝不保留状态必须留状态的用thread_local或者独立状态对象。6.3 结合项目场景的最终选择指南我给团队定过一个简易决策流程先问链会不会变——不会变走编译期链会变再问变的方式——是运行时动态插拔函数式链即可还是配置驱动上 ChainBuilder。然后问执行语义——找一个人处理是经典链还是所有人都处理一遍是广播链。最后问性能——热路径上百纳秒能忍就正常实现不能忍就编译期链。这样梳理完其实很多职责链模式不够用的抱怨都消失了。不是模式有问题而是你没找到对的变体。模式不是铁板一块它是一族解决相似问题的工具集合我们需要的只是按场景挑出最锋利的那把。说到最后我自己现在写 C 项目默认起手式基本是函数式链因为它成本最低、可读性最好。只有当我确定某个链路是热路径时才会升级到编译期链。至于可装配链和广播链更多是被具体业务逼出来的特殊工具。职责链这个模式我越用越觉得它的精神内核不是那条链而是把判断逻辑从业务流程中抽离出来——只要你理解了这一点具体用哪种变体其实都不是什么大事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑