ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++职责链模式高级实战:从动态编排到协程异步化

C++职责链模式高级实战:从动态编排到协程异步化 职责链模式在C里属于那种拿起来容易、用好了很难的设计模式。很多人知道它的教科书写法一个抽象Handler类每个子类维护一个next指针handle的时候要么自己处理要么丢给下一个处理器。但真到了生产环境你会发现这套东西很快就撑不住——链接死了、顺序调不动、中间想插入一个切面逻辑还得改一堆类。这篇文章不会重复那些网上一搜一大把的基础教程而是基于我做网关服务、登录校验管道和异步中间件框架的实战经验把职责链模式在C里的高级玩法一次讲透动态编排、泛型化、编译期链、协程异步化以及那些教科书永远不会告诉你的坑和排查手段。适合已经知道设计模式基本概念、想在真实项目里把职责链真正用起来的C开发者。1. 先搞清楚职责链模式到底在解决什么问题1.1 一句话说清职责链职责链模式Chain of Responsibility是GoF二十三种设计模式里的行为型模式核心思想就是八个字请求发起者和处理者解耦。传统调用方式里客户端必须清楚谁能处理自己的请求然后把调用关系写死职责链反过来做——客户端只把请求丢进一条链里链上的每个节点自己判断这事该不该我来该我处理就处理不该我就往后传。这个概念用生活场景特别好理解你去政务大厅办事取号窗口不办业务只分流综合窗口看你的材料判断是社保窗口、发票窗口还是注销窗口的事每个窗口处理不了就往下一个窗口推。整个流程里你的诉求从来没变过变的是沿途经过的节点而最终哪个窗口把事情办成取决于每个窗口自己的判断逻辑。在C项目里这个模式最常见的身影是中间件管道、请求校验链、消息分发器、日志级别过滤、UI事件透传。我做过一个网关服务一条请求进来要过鉴权、限流、参数校验、权限过滤、业务路由整整五层逻辑。如果不用职责链这五层嵌套写出来就是一场噩梦——每加一层新校验所有调用点都要跟着改。用职责链之后加一个风控检查节点只是往链里多塞一段代码的事。1.2 什么场景适合职责链什么场景千万别用适合用职责链的信号有三个第一同一类请求可能存在多个处理者但运行时才知道谁会接单第二处理逻辑有明确的先后顺序且这个顺序可能动态调整第三你希望新增一种处理能力时不动现有任何处理代码。前两个对应解耦第三个对应开闭原则——对扩展开放对修改关闭。反过来三个信号不满足就别硬套。如果处理者只有一个且永远不会变直接if判断就行引入职责链是纯增加复杂度如果所有处理者都必须执行、不需要中断也不需要跳过那你需要的其实是管道模式Pipeline和职责链谁处理谁中断的语义有微妙差别如果请求量极大、链路极长每个节点还都走虚函数间接调用性能瓶颈会真实存在这时候可以考虑后面讲到的编译期链。一句话职责链的价值在于每个节点有自主决定权。如果没有这个判断-传递-中断的博弈过程它就没存在的意义。2. 教科书实现我为什么劝你别直接抄2.1 经典写法里藏着的三个硬伤先看网上一抓一大把的教科书写法// 抽象处理器 class Handler { public: virtual ~Handler() default; void setNext(std::shared_ptrHandler next) { m_next std::move(next); } virtual void handle(Request req) { if (m_next) { m_next-handle(req); } } protected: std::shared_ptrHandler m_next; }; // 具体的鉴权处理器 class AuthHandler final : public Handler { public: void handle(Request req) override { if (req.userId 0) { std::cout 未登录请求被拦截\n; return; // 不再往下一个节点传 } req.authenticated true; std::cout 鉴权通过\n; if (m_next) m_next-handle(req); // 手动向后传递 } };这段代码本身没啥毛病但它把问题都写在看不见的地方了。硬伤一链是死的。链的结构靠每个节点的m_next在初始化时手动串起来串完就固定了。你想在运行时按优先级调整顺序对不起得先把老链拆了重串你想在中间插入一个统计耗时的切面节点得改初始化代码跑起来之后想插没门。硬伤二职责传递靠自觉。每个子类在handle的结尾必须记得调用m_next-handle()但编译器根本不会帮你检查这个。我见过不止一次有人写了分支处理return之前忘了调用next于是整个链路静默断了——前面的请求成功处理后面的逻辑全没执行线上排查排到怀疑人生。硬伤三递归传递栈随链长线性增长。一条四五十节点的链每个节点一次方法调用深了就压栈。虽然现代默认栈8MB通常扛得住但如果你在递归场景外层再套这么一条链栈溢出就是时间问题。2.2 用容器把链做活动态职责链我自己在项目里几乎不用上面的指针串联写法而是用容器存储顺序遍历实现职责链。核心思路很简单既然链不过是一串有顺序的处理器那直接用std::vector装起来遍历执行不就行了class Pipeline { public: using HandlerFunc std::functionbool(Request); // 追加到链尾 void append(HandlerFunc func) { m_handlers.push_back(std::move(func)); } // 插到链头高优先级 void prepend(HandlerFunc func) { m_handlers.insert(m_handlers.begin(), std::move(func)); } // 按名字插到某个节点之前/之后 void insertBefore(std::string_view target, HandlerFunc func); // 移除指定节点 void remove(std::string_view name); // 顺序执行整条链 bool run(Request req) const { for (const auto func : m_handlers) { if (!func(req)) { return false; // 链被中断 } } return true; } private: std::vectorHandlerFunc m_handlers; };这个写法的收益非常直接运行期的增删改查全都有。加一个节点就是pipeline.append(...)想临时屏蔽一个风控节点就是pipeline.remove(risk)不用动任何业务类。每个处理器用lambda封装顺手把名字也带上pipeline.append([](Request req) { std::cout 节点[1]限流检查 start\n; if (req.userId 0) { return false; // 限流拒绝 } return true; });等等这个写法有个新问题节点的名字丢了。lambda自身没法携带身份信息remove和insertBefore根本没法工作。解决方法是引入一个轻量结构体作为链上元素struct ChainNode { std::string name; // 节点身份 HandlerFunc handler; // 处理逻辑 }; class Pipeline { // ... private: std::vectorChainNode m_nodes; };于是append改为接受name和HandlerFunc两个参数insertBefore、remove都按name操作。这样一来职责链从指针串起来的静态链表进化成了可动态编排的执行序列这是第一个真正能落地的进阶点。2.3 链构建器把装配过程也封装起来链活了之后下一个问题是谁来装配这些节点。直接在业务代码里append七八个节点局部变量一多还是乱。更优雅的做法是提供一个链构建器Builder风格把节点的装配和链的执行彻底分开class RequestPipeline { public: RequestPipeline auth() { m_pipeline.append(auth, [](Request req) { if (req.userId 0) return false; req.authenticated true; return true; }); return *this; } RequestPipeline rateLimit(int maxTokens) { // 使用状态捕获令牌桶参数 auto bucket std::make_sharedTokenBucket(maxTokens); m_pipeline.append(rate_limit, [bucket](Request req) { return bucket-tryAcquire(); }); return *this; } RequestPipeline validate() { m_pipeline.append(validate, [](Request req) { return !req.action.empty(); }); return *this; } bool run(Request req) const { return m_pipeline.run(req); } private: Pipeline m_pipeline; }; // 使用 RequestPipeline builder; builder.auth().rateLimit(100).validate(); bool ok builder.run(req);链构建器还有个隐藏好处用的链和构建的链可以分离。比如A接口需要authrateLimitB接口需要authvalidate构建器内部可以缓存几种预设链buildForAdmin()、buildForGuest()运行时按请求类型挑一条执行这个灵活性是传统指针链给不了的。3. 类型擦除与泛型化从面向Handler接口到面向行为3.1 为什么std::function是职责链的最佳搭档第二节的HandlerFunc直接用了std::functionbool(Request)很多C老手看到第一反应是这玩意儿有虚函数调用开销吧。确实std::function本质是类型擦除内部用一个类型化的包装器模拟虚调用比裸虚函数略重一点点。但对绝大多数业务链来说这个开销可以忽略——一次std::function调用大概是几纳秒级别的成本你链上随便一个校验跑一次数据库查询就是毫秒级。真正值得关注的是std::function带来的收益它让处理器的形态彻底解放了。以前你得继承Handler、重写handle现在任何可调用对象都能作为链节点——普通函数、lambda、仿函数、std::bind产生的绑定表达式甚至另一个链Pipeline嵌套。节点之间没有继承关系只有行为契约bool(Request)这比继承体系更贴合职责这个词的本质——管它是什么类能处理这个请求就行。3.2 模板链在编译期把整条链固化成零开销如果只是容器std::function性能敏感场景可能还是不够极致。C17的折叠表达式fold expression给了我们第三个维度编译期职责链。templatetypename... Handlers class CompileTimeChain { public: explicit CompileTimeChain(Handlers... handlers) : m_handlers(std::move(handlers)...) {} bool run(Request req) { // C17 左折叠从左到右依次执行短路 return (std::getHandlers(m_handlers).handle(req) ...); } private: std::tupleHandlers... m_handlers; }; // 使用 struct Auth { bool handle(Request req) { if (req.userId 0) return false; req.authenticated true; return true; } }; struct RateLimit { bool handle(Request req) { return req.userId 0; } }; CompileTimeChainAuth, RateLimit chain; bool ok chain.run(req);这条链完全在编译期定型没有虚函数、没有std::function、没有堆分配( ...)还会产生短路求值——某个节点返回false后续节点连调用都不会发生。代价是链的结构写死在类型里无法在运行时增删。所以我的建议是运行期需要动态改链用Pipeline结构稳定、性能苛刻用CompileTimeChain。两种实现服务于两套需求不是谁替代谁。3.3 异构处理器让普通函数和类对象混排模板链的Handlers...天然支持异构——CompileTimeChainAuth, RateLimit里Auth和RateLimit是不同类只要它们都有handle(Request)成员函数就能进链。但业务里经常混着简单函数和有状态类比如链里既有纯函数式的黑名单过滤又有需要内部状态的令牌桶限流。让它们都实现成员函数临时写一个小类又嫌烦。一个折中方案是让CompileTimeChain同时接受可调用对象templatetypename... Callables class Chain { public: // 每个元素通过 std::invoke 统一调用 bool run(Request req) { return (std::invoke(std::getCallables(m_handlers), req) ...); } private: std::tupleCallables... m_handlers; }; auto chain Chain( [](Request req) { return req.userId ! 0; }, RateLimit{}, [](Request req) { req.authenticated true; return true; } );std::invoke是C17给可调用对象的统一门票lambda直接调用仿函数调用operator()成员函数指针自动绑定第一个参数。这样一来你链上的每个节点可以是代码层面的任意形态职责链从继承的链彻底变成了行为的链。4. 返回值、上下文与短路语义把链的控制权握在自己手里4.1 从bool返回值看职责链的三种执行风格第二节代码的bool run(Request)默认了某个节点返回false就中断整条链。这是我最常用的语义但职责链实际上有三种执行风格选错会非常别扭执行风格语义典型场景短路执行遇到第一个我要处理的节点就停后面的不再执行事件捕获、单一处理者全链执行每个节点都执行可以修改同一个上下文中间件管道、日志过滤断链执行节点返回失败就中断但中断后可带原因请求校验、审批流为了支撑这三种风格我把返回值从bool升级成一个三态枚举enum class HandleResult { Continue, // 本节点处理完毕继续下一个 Stop, // 终止整条链但不算失败有人接管了 Reject, // 终止整条链并标记失败 }; using HandlerFunc std::functionHandleResult(Request);于是管道执行变成bool run(Request req) const { for (const auto node : m_nodes) { auto result node.handler(req); if (result HandleResult::Stop) return true; // 有人接管链结束 if (result HandleResult::Reject) return false; // 被拒了 } return true; // 全员放行 }这个小小的升级让链的语义从要么真要么假变成了业务可感知的三种走向实际开发里写校验链的时候尤其好用——未登录是Reject这个请求被Mock处理器接管是Stop普通校验通过是Continue逻辑一目了然。4.2 上下文对象让链共享状态而不污染接口链上的节点常常需要共享数据鉴权节点写入当前用户ID限流节点记下剩余令牌业务节点读取这两个字段。如果每个节点都自己存这些数据链就没法协作。所以职责链的进阶用法离不开一个上下文对象Contextstruct RequestContext { int userId 0; bool authenticated false; std::string action; std::vectorstd::string trace; // 调试用记录经过的节点 }; using HandlerFunc std::functionHandleResult(RequestContext);每个节点读写同一个RequestContext节点之间通过上下文隐式通信。这有点像现实流程单——每个窗口在自己的格子盖章、填意见整个流程单从头传到底。上下文对象还顺带解决了调试问题节点往trace里塞一个名字出问题直接看流程单哪一步断了。4.3 短路之后还需要清理引入finally节点断链语义有个副作用中断后前面节点分配的资源没人收尾。比如限流节点给请求申请了一个锁后面的业务校验失败了锁谁来释放正常思路是RAII锁随作用域自动释放但如果前面节点持有的是进程级共享资源数据库连接、文件句柄就不行了。我习惯在Pipeline里额外支持一类finally节点——不管主链结果如何最后一定执行清理节点void addFinally(HandlerFunc func) { m_finallyNodes.push_back(std::move(func)); } bool run(RequestContext req) { bool ok dispatch(m_nodes, req); // 主链 dispatch(m_finallyNodes, req); // 清理链无条件执行 return ok; }这个设计借鉴了语言里try-finally的思想但放在职责链层面统一管理比每个节点各自塞try-catch干净得多。注意清理链节点不能返回Reject否则会把主链的成功结果覆盖掉——我通常让清理链只吞日志不放行判断。5. 高级实战把异步处理融入职责链5.1 让处理器返回future一个不给力的异步改造先说我踩过的坑。最早我想让某几个耗时校验节点异步化——比如风控系统要调外部接口300ms端到端延迟总不能同步阻塞整条链吧。我直接把HandlerFunc的返回类型改成std::futureboolusing HandlerFunc std::functionstd::futurebool(RequestContext); // 执行时却没等 future 就跑了下一个节点这个方案有个致命伤除非每个future都立刻wait否则链的顺序性就崩了——鉴权还没完成限流节点可能已经把请求当成未登录用户拒了。等所有future都wait又退化成同步阻塞还凭空多了一次future的堆分配纯亏。5.2 C20协程与职责链顺序执行为什么能异步化后来我把职责链搬到了C20协程上思路一下就顺了协程让顺序的代码挂起而不阻塞线程成为可能。// 每个处理器从返回 future 变成返回 task且内部可以 co_await class AsyncPipeline { public: using HandlerFunc std::functioncoro::taskbool(RequestContext); coro::taskbool run(RequestContext req) const { for (const auto node : m_nodes) { auto ok co_await node.handler(req); // 挂起等异步结果 if (!ok) co_return false; } co_return true; } }; // 使用 coro::taskbool process(RequestContext req, AsyncPipeline pipeline) { auto ok co_await pipeline.run(req); std::cout 链执行 (ok ? 成功 : 失败) \n; co_return ok; }协程版本的最大价值是执行顺序依然严格co_await让出线程异步结果回来后再恢复执行下一个节点。外部调用者看到的行为和同步链完全一致但线程不再被300ms的等待白白占住。我在网关项目里用这个方案把一批I/O密集的校验节点从同步改成协程后同样的线程池承载的并发量直接翻了一倍还多。注意协程链必须小心节点内部不能并发修改同一个RequestContext。职责链的语义是顺序执行协程只是允许等待时不占线程不是让节点并发跑。想并发加速应该在节点内部自己开任务、自己等待聚合。5.3 异步链的事件驱动姿势基于回调的推式链如果项目还没升级到C20可以用经典的异步回调风格模拟职责链。控制权反转是关键——每个节点处理完主动调用回调告知我好了/我拒绝了using Next std::functionvoid(); using Done std::functionvoid(bool ok); using AsyncHandlerFunc std::functionvoid(RequestContext, Next, Done); class AsyncPipeline { public: void run(RequestContext req, Done done) { if (m_nodes.empty()) { done(true); return; } m_nodes[0](req, makeNext(req, 0, done), std::move(done)); } private: Next makeNext(RequestContext req, size_t index, Done done) { return [this, req, index, done]() mutable { if (index 1 m_nodes.size()) { done(true); return; } m_nodes[index 1](req, makeNext(req, index 1, done), done); }; } std::vectorAsyncHandlerFunc m_nodes; };这个回调推进的写法在手撸中间件框架时很常见Node.js的Express就是这种模型节点内部既可以选择调用next()继续也可以直接调done(false)中断。缺点也明显代码变成洋葱模型心智负担重深一环就多一层回调嵌套出错了栈信息都不好看。所以我的建议是新项目无脑用协程老项目要动这块再考虑回调方案。6. 实战案例复盘用职责链做一个登录校验管道6.1 需求拆解用前面的技术做一个完整可运行的登录校验管道。场景服务端收到一个登录请求要求依次通过五关黑名单检查IP或账号在黑名单里直接拒。鉴权登录必须带合法的Token解析出用户ID。限流同一用户ID一分钟最多10次请求超了就拒。参数校验action必须非空必须是允许的操作集合之一。业务判断查询用户账户状态禁用账号拒。每一关失败要有明确的错误码方便客户端提示。6.2 完整实现#include iostream #include string #include vector #include functional #include unordered_set #include chrono struct RequestContext { int userId 0; std::string token; std::string action; std::string errorMsg; std::vectorstd::string trace; }; enum class HandleResult { Continue, Stop, Reject }; using HandlerFunc std::functionHandleResult(RequestContext); class LoginPipeline { public: void add(std::string name, HandlerFunc func) { m_nodes.push_back({std::move(name), std::move(func)}); } bool run(RequestContext req) { for (const auto node : m_nodes) { req.trace.push_back(node.name); auto result node.func(req); if (result HandleResult::Stop) return true; if (result HandleResult::Reject) return false; } return true; } private: struct Node { std::string name; HandlerFunc func; }; std::vectorNode m_nodes; };核心链路LoginPipeline buildPipeline() { LoginPipeline pipeline; // 1. 黑名单检查 pipeline.add(blacklist, [](RequestContext req) { static const std::unordered_setint blackList {666, 888}; if (blackList.count(req.userId)) { req.errorMsg 账号已被拉黑; return HandleResult::Reject; } return HandleResult::Continue; }); // 2. 鉴权解析 token 得到 userId pipeline.add(auth, [](RequestContext req) { if (req.token.size() 6) { req.errorMsg token 无效; return HandleResult::Reject; } // 真实项目中这里应该做签名校验 req.userId static_castint(req.token.back()) - 0 1; return HandleResult::Continue; }); // 3. 限流每用户每分钟最多 10 次 pipeline.add(rate_limit, [](RequestContext req) { auto counter getCounter(req.userId); // 简化只保留概念 if (counter 10) { req.errorMsg 请求过于频繁; return HandleResult::Reject; } counter; return HandleResult::Continue; }); // 4. 参数校验 pipeline.add(validate, [](RequestContext req) { static const std::unordered_setstd::string allowed {login, logout}; if (!allowed.count(req.action)) { req.errorMsg 非法操作; return HandleResult::Reject; } return HandleResult::Continue; }); // 5. 业务状态判断 pipeline.add(account_status, [](RequestContext req) { if (req.userId 2) { req.errorMsg 账号已禁用; return HandleResult::Reject; } return HandleResult::Continue; }); return pipeline; }调用与输出int main() { auto pipeline buildPipeline(); RequestContext req; req.token abc123; req.action login; bool ok pipeline.run(req); std::cout 结果: (ok ? 通过 : 被拒) \n; std::cout 错误: req.errorMsg \n; std::cout 链路: ; for (const auto node : req.trace) { std::cout node - ; } std::cout end\n; return 0; }运行输出结果: 通过 错误: 链路: blacklist - auth - rate_limit - validate - account_status - end这个案例的要点不是代码本身多高级而是它把前面所有技巧整合了三态结果让拒绝原因能随链传递Context贯穿让trace和errorMsg共享Pipeline容器让五关的增删改都是append一行的事。线上如果要临时加一个设备风控节点直接在buildPipeline里再pipeline.add(device_risk, ...)就完事了调用方main一行都不用改——这就是职责链的价值体现。7. 职责链模式在C实战里的8个坑与排查技巧7.1 坑一链静默断裂没人知道后面没执行这是最隐蔽的坑。某个节点return了却没设置errorMsg下游节点因此没跑线上行为表现为功能时好时坏。排查方法只有一个让trace永远可观测。每个节点进入和退出都往Context里记一条问题链路一眼就能看出来。我在run里加了req.trace.push_back(node.name)这只是最粗糙的版本更细的可以记耗时auto start std::chrono::steady_clock::now(); auto result node.func(req); req.trace.push_back( node.name : std::to_string( std::chrono::duration_caststd::chrono::microseconds( std::chrono::steady_clock::now() - start).count() ) us );有了耗时哪一级拉高了接口延迟打开日志秒定位不用专门上性能分析器。7.2 坑二节点返回值写反短路变成全链执行这是我常看到的bug明明想校验失败就中断节点里却写return HandleResult::Continue。三态枚举的名字太容易混尤其从bool迁移过来的人容易把真成功和继续划等号。我的建议是枚举命名再显式一点比如Pass、Break、Fail并在run里加断言节点返回Fail时必须带上非空errorMsgif (result HandleResult::Fail) { assert(!req.errorMsg.empty() Fail 节点必须写明错误原因); return false; }Debug版立刻炸Release版也能在trace里看到。7.3 坑三循环引用导致shared_ptr泄漏如果用了老式指针链setNext存的是shared_ptrHandler而某个Handler又被外部shared_ptr持有一旦两条链之间出现互相引用A的next是BB的next是A就是典型的循环引用——引用计数永远到不了0内存泄漏。解决思路有三个把next改成unique_ptr从语义上禁止共享或者干脆放弃指针链用第二节的容器方案节点不持有彼此循环引用从根上消失如果非要shared_ptr就拆成weak_ptr。我在新代码里永远选容器方案理由已经说透了——指针链除了省一个vector的开销没有别的优势而这开销根本不值得换一套心智负担。7.4 坑四递归传递导致的栈溢出教科书链是递归调用handle→next-handle→next-handle...。五十个节点的链压栈五十层乍看不危险但要是在链的某个节点里又递归处理了一批子请求叠加起来就能把栈打穿。迭代版本的容器Pipeline天然免疫这个问题——深度永远是1因为for循环不递归。这也是容器方案的又一个隐藏收益。7.5 坑五节点异常打断链状态不一致C异常可以让链直接崩溃某节点throw std::runtime_error后续节点全不执行Context里可能已经写了一半字段处在一个半初始化状态。我的做法是Pipeline统一接住异常bool run(RequestContext req) noexcept { try { for (const auto node : m_nodes) { auto result node.func(req); // ... } return true; } catch (const std::exception e) { req.errorMsg std::string(链异常: ) e.what(); return false; } }注意异常一定要记录到trace里不然你又得猜哪个节点抛的。另外别在run里吞掉所有异常就完事——日志必须打因为异常往往是bug的信号。7.6 坑六多线程共享同一PipelinePipeline如果被多个线程同时run内部节点如果引用了共享状态比如令牌桶要小心数据竞争。但注意区分两件事Pipeline容器本身是否线程安全不重要它是只读的配置对象初始化后别改就行节点内部的状态才是竞争的重灾区。我的约定是节点状态要么自带上锁要么用原子操作要么干脆做成每次请求独立构造的无状态对象。拿限流来说令牌桶内部计数器必须加锁否则并发请求会把计数算错。7.7 坑七链顺序敏感却没人维护顺序容器方案让顺序变得太容易调整反而引出新问题业务对顺序的隐性依赖被忽略了。比如限流节点必须在鉴权之后因为它要按userId计数黑名单必须在鉴权之前因为黑名单里有些是未登录IP。这种顺序约束散落在各个节点的注释里新人上来一调整就出bug。解决方法是把顺序约束显式化在每个节点注册时声明依赖关系struct NodeSpec { std::string name; std::string dependsOn; // 必须在此节点之后 HandlerFunc func; };Pipeline在add时检查依赖是否满足不满足直接抛配置错误。这个小小的显式约束能帮你拦住一大批手滑换顺序导致的隐性bug。7.8 坑八测试职责链时只测节点不测装配节点单独单测全部通过合起来链里顺序错了、中断条件错了线上照样炸。原因在于职责链的价值恰恰在装配。我的实践是给Pipeline写链路语义测试构造特定请求断言执行的trace顺序、断言在哪个节点被中断、断言Context被改成了什么。测试用例优先写黑名单请求必须在auth之前被拒这类顺序语义用例而不是auth函数对空token返回false这种节点内聚性用例。只有把装配顺序测住了职责链的改动才是安全的。8. 我个人用的职责链心法总结写这篇文章的过程其实也把我自己这么多年踩坑的体会重新捋了一遍。设计模式这东西看十遍理论不如在真实代码里栽一次跟头。职责链模式最打动我的地方不是那个谁处理谁中断的经典语义而是它在C里可以演化出这么多种形态指针静态链、容器动态链、模板编译期链、协程异步链。每种形态对应一种场合没有哪个是银弹——动态灵活和零开销、运行时编排和编译期固定都是一体两面的取舍重要的是你能意识到这条谱系的存在按自己的场景选位。最后分享一个我一直在用的小习惯凡是用了职责链的地方一定要在代码注释里画一张链的装配图谱。不是复杂的设计图就一副简单的清单——第1个节点是谁、它管什么、哪个节点希望谁中断、哪些节点不能顺序颠倒。因为职责链的代码读起来很反直觉你看到的是一个一个独立的小函数它们的协作关系完全不在函数内部而在装配代码里。一张图谱能让半年后的你自己和之后接手的新人少猜很久。
返回列表