1. 从一段臃肿的审批代码说起:不用职责链模式会怎样
先说个我自己的经历。前几年在做一个内部工单系统,里面有个审批模块,需求是这样的:工单提交之后,要根据金额走不同层级的审批——金额小于1000块的直接通过,1000到5000的需要组长审批,5000到20000需要部门经理审批,超过20000还要往上走总监。当时项目比较赶,我第一版就按最直白的方式写了:
bool ApproveService::process(const Order& order) { if (order.amount < 1000) { // 直接通过 order.status = APPROVED; return true; } else if (order.amount >= 1000 && order.amount < 5000) { // 找组长 return teamLeader->approve(order); } else if (order.amount >= 5000 && order.amount < 20000) { // 找部门经理 return manager->approve(order); } else { // 找总监 return director->approve(order); } }写的时候觉得挺爽的,逻辑清清楚楚。结果上线不到一个月,需求就开始变了:先是加了“紧急工单优先走总监通道”,然后又说“金额超过50000的除了总监还要抄送财务”,接着技术负责人说想加一个“数据合规校验节点”插在审批最前面,再后来产品经理提出来要支持“自定义审批链”——运维同学可以在后台自己拖拽配置审批流程。
那段时间我每次改这个函数都提心吊胆。if-else嵌套越来越深,方法越来越长,最后甚至出现了一个奇奇怪怪的bug:因为审批人对象在某个分支里没有初始化,某些金额区间的工单走不到审批这一步就抛了空指针。我那时才意识到,这段代码最大的问题不是难写,而是审批流程本身是一条链——请求沿着链依次经过每个审批节点,直到有人处理它。而我却用一个硬编码的if-else把它拍平了。
这时候就该上设计模式了。职责链模式(Chain of Responsibility)的定义很简单:让多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。文字有点绕,但说白了就一句话:把那些“先后判断、依次处理”的逻辑,从密不透风的if-else里拆出来,变成一个个独立的处理器节点,再把它们串起来。
本篇我主要分享四点:职责链模式的结构和C++实现、落地过程中C++特有的一些细节考量、几个真实的应用场景,以及我在项目中踩过的坑。不管你是刚接触设计模式的新手,还是写了两三年C++正在整理代码结构的开发者,这篇应该都能给你一些拿得走的东西。
有一点先说清楚:设计模式不是银弹。职责链模式如果滥用,反而会让代码变得支离破碎——明明三个if能解决的事,非要建五个类,那是自找麻烦。所以文章最后我也会聊一聊“什么情况下不该用”。
2. 设计模式不是画类图:职责链的骨架到底是啥
2.1 三要素:Handler、ConcreteHandler、Client
职责链的类图在网上随便一搜就是一大把,GoF经典结构就是几个人物:一个抽象的Handler,若干个具体的ConcreteHandler,还有一个组合这条链的Client。但类图是静态的,真正要理解这个模式,得看它在运行时的行为。
三个角色各干各的事:
- Handler(抽象处理者):定义一个处理请求的接口,并且持有下一个处理节点的指针。这是整个链的“接口契约”。在C++里,它通常是一个含有纯虚函数的抽象基类,或者是一个std::function的包装类型。
- ConcreteHandler(具体处理者):实现自己的处理逻辑。如果自己能处理,就处理掉;处理不了,就把请求转发给下一个节点。注意:“能处理”和“不能处理”的边界,每个节点自己说了算,这恰恰是职责链最灵活的地方。
- Client(客户端):组装链,并把初始请求发给链头。客户端只跟第一个节点打交道,完全不关心后面挂了几个节点。
这里有个很多人容易忽略的点:在职责链模式里,请求的流向是“链式”的,但客户端看到的只有一个入口。发送者不需要知道这单子最终是谁审批的、经过了哪些中间环节——它只负责把请求丢进链里。这就把一个复杂的多级判断逻辑,变成了一个单一入口的调用,复杂度被封装在链内部。
2.2 为什么说“每个节点自己决定是否处理”是关键
很多初学者会把职责链理解成“责任传递链”或者“管道”,其实差了一点。管道的语义是:每个节点都会处理数据,处理后传给下一个(有点像流水线)。而职责链的经典语义是:请求沿着链走,一旦某个节点处理了,链路就终止,后面的节点不再执行。
这两种语义在业务上差别很大。审批流就是典型的“短路”式——组长批了就不用再找经理。而日志系统则是“全链”式——debug日志既要写控制台又要写文件还要上报远程,每个节点都处理一遍。
所以你在实现职责链的时候,先想清楚你的业务是“找到第一个能处理的人”还是“每个人都要过一遍手”。这决定了节点的编排方式和返回语义。GoF原书里说的是“直到有一个对象处理它为止”,也就是短路式,但实际项目中两种都很常见,别把它当死规矩。
2.3 一个生活化的类比:食堂打饭的窗口
理解职责链最直观的方式其实是类比日常生活。你去食堂打饭,拿着餐盘走到第一个窗口,问“有红烧肉吗”,阿姨说没有,指了指第二个窗口;第二个窗口也说没有,让你去第三个窗口;第三个窗口阿姨说“有”,然后给你打了一份。
你(客户端)不需要知道红烧肉在哪个窗口,你只从第一个窗口开始问,没菜就顺着指的方向走,直到有人给你打菜。这就是职责链——每个窗口就是链上的一个节点,它要么自己处理(打菜),要么把请求往后传(指路)。
类比的意义在于,你能立刻感受到这个模式的两个优点:调用侧极其简单(只需要找第一个窗口),节点之间完全独立(每个窗口只需要知道“下一个窗口在哪”)。
3. 写一个能跑的C++版本:审批链的落地实现
3.1 经典写法:抽象基类 + 派生节点
下面我给出一个完整的、可以编译运行的C++实现。就用文章开头的审批场景,但会比最初的if-else版本干净得多。
#include <iostream> #include <memory> #include <string> #include <utility> // 工单请求 struct Order { int id; double amount; // 金额 bool isUrgent; // 是否紧急 }; // 1. 抽象处理者:审批节点基类 class Approver { public: explicit Approver(std::string name) : name_(std::move(name)), next_(nullptr) {} virtual ~Approver() = default; // 设置链上的下一个节点 void setNext(std::shared_ptr<Approver> next) { next_ = std::move(next); } // 处理请求的入口 void handle(const Order& order) { if (canApprove(order)) { approve(order); } else if (next_) { next_->handle(order); } else { std::cout << "[System] 无节点可处理该请求,工单 " << order.id << " 被挂起" << std::endl; } } protected: // 每个节点自己判断是否能处理 virtual bool canApprove(const Order& order) = 0; // 具体的审批动作 virtual void approve(const Order& order) = 0; std::string name_; std::shared_ptr<Approver> next_; }; // 2. 具体处理者A:组长 class TeamLeader : public Approver { public: TeamLeader() : Approver("TeamLeader") {} protected: bool canApprove(const Order& order) override { return order.amount < 5000; } void approve(const Order& order) override { std::cout << "[TeamLeader] 审批通过,工单ID: " << order.id << ",金额: " << order.amount << std::endl; } }; // 3. 具体处理者B:部门经理 class DepartmentManager : public Approver { public: DepartmentManager() : Approver("DepartmentManager") {} protected: bool canApprove(const Order& order) override { return order.amount >= 5000 && order.amount < 20000; } void approve(const Order& order) override { std::cout << "[DepartmentManager] 审批通过,工单ID: " << order.id << ",金额: " << order.amount << std::endl; } }; // 4. 具体处理者C:总监 class Director : public Approver { public: Director() : Approver("Director") {} protected: bool canApprove(const Order& order) override { return order.amount >= 20000; } void approve(const Order& order) override { std::cout << "[Director] 审批通过,工单ID: " << order.id << ",金额: " << order.amount << std::endl; } }; // 5. 客户端:组装链并触发 int main() { auto teamLeader = std::make_shared<TeamLeader>(); auto manager = std::make_shared<DepartmentManager>(); auto director = std::make_shared<Director>(); // 组装链路:组长 -> 经理 -> 总监 teamLeader->setNext(manager); manager->setNext(director); Order order1{1001, 3000, false}; Order order2{1002, 8000, false}; Order order3{1003, 50000, false}; std::cout << "--- 处理工单 1001 ---" << std::endl; teamLeader->handle(order1); std::cout << "--- 处理工单 1002 ---" << std::endl; teamLeader->handle(order2); std::cout << "--- 处理工单 1003 ---" << std::endl; teamLeader->handle(order3); return 0; }这段代码的运行结果如下:
--- 处理工单 1001 --- [TeamLeader] 审批通过,工单ID: 1001,金额: 3000 --- 处理工单 1002 --- [DepartmentManager] 审批通过,工单ID: 1002,金额: 8000 --- 处理工单 1003 --- [Director] 审批通过,工单ID: 1003,金额: 50000注意几个细节。我把canApprove和approve拆成了两个虚函数,这是个小设计点:判断和处理分离,以后如果你想做“记录日志后处理”或者“先校验再处理”,直接重写handle或approve就行,不用动判断逻辑。另外我在链尾加了一个兜底分支——没有任何节点能处理时,打印挂起消息。这个兜底逻辑非常重要,实际项目中请求“走完整个链都没人处理”是常态,不是异常,你一定要对这种情况有明确的行为定义,否则就是静默吞掉请求,线上排查会非常痛苦。
3.2 用现代C++改写:std::function与lambda派发器
基类+派生类是教科书写法,但如果你项目里只有两三种处理器,不想为每个处理器单独立一个类,可以用std::function来做轻量级节点。C++11之后这个写法很常见,代码量少很多,而且特别适合“处理器本身就是几行lambda”的场景:
#include <iostream> #include <functional> #include <memory> #include <vector> struct Order { int id; double amount; }; using Handler = std::function<bool(const Order&)>; class Chain { public: void append(Handler h) { handlers_.push_back(std::move(h)); } void handle(const Order& order) { for (auto& h : handlers_) { if (h(order)) { return; // 短路:某个处理器处理了请求 } } std::cout << "[Chain] 无处理器可处理,请求被丢弃" << std::endl; } private: std::vector<Handler> handlers_; }; int main() { Chain chain; chain.append([](const Order& order) { if (order.amount < 1000) { std::cout << "自动通过,金额: " << order.amount << std::endl; return true; } return false; }); chain.append([](const Order& order) { if (order.amount >= 1000 && order.amount < 5000) { std::cout << "组长审批,金额: " << order.amount << std::endl; return true; } return false; }); chain.append([](const Order& order) { std::cout << "默认处理:人工介入,金额: " << order.amount << std::endl; return true; }); chain.handle(Order{1, 500}); chain.handle(Order{2, 3000}); chain.handle(Order{3, 99999}); return 0; }这里我用了std::vector<Handler>代替链表结构。你可能会问:这还算职责链吗?算的。职责链模式的核心是“请求沿着处理节点序列依次传递直到被处理”,底层用链表还是数组,这是实现细节。用vector的好处是遍历快、代码直观,坏处是你很难在运行时动态“插入”节点——但大多数业务并不需要真的动态插入,配置链的顺序在初始化时就定死了。
我个人在实际项目里的习惯是:处理器逻辑简单、数量少、不太可能被外部扩展的时候用std::function版本;处理器有内部状态、需要复用、逻辑复杂的时候用基类版本。不要一上来就觉得基类版本才“正宗”,代码是给人读的,不是给设计模式书考的。
3.3 生命周期管理:shared_ptr还是裸指针
这是C++实现职责链时一个非常具体的问题。Java、C#里你new一个对象不用太操心释放,C++不行。链上的节点在运行期会被多次跳转访问,如果你用裸指针,组装链的代码和触发链的代码在不同的作用域里,稍不注意就会出现悬垂指针。
我建议统一用std::shared_ptr管理节点,链的“next”指针也用shared_ptr。理由很简单:链节点的生命周期天然是“多段共享”的——某个节点可能同时被两条链引用(比如总监同时挂在一级审批链和紧急审批链上),你很难判断谁该最后释放它。shared_ptr引用计数能省掉这部分心智负担。
class Approver { std::shared_ptr<Approver> next_; public: void setNext(std::shared_ptr<Approver> next) { next_ = std::move(next); } };一个要注意的坑是:不要用shared_ptr循环引用。如果A的next指向B,B又持有A的shared_ptr,那两者的引用计数永远不为零,内存就泄漏了。职责链通常是单向链表,原则上不会形成环,但如果你在节点里顺手存了一个“上级节点”反向指针,就要非常小心了。我见过一个事故:有人为了让节点能“回溯”到链头,每个节点都存了一个头节点的shared_ptr,结果头节点析构时引用计数直接被抬到了3,整条链怎么删都删不掉。如果是这种双向引用需求,请用std::weak_ptr打破循环。
4. 真实项目的三种应用场景:日志、中间件与事件分发
4.1 日志系统中的多级过滤链
职责链在日志框架里几乎是标配。以我熟悉的轻量级日志组件为例,一条日志从产生到落盘,要经过“级别过滤器→格式转换器→输出器(控制台/文件/远程)”,每个环节都是一个处理节点。如果按照传统if-else写,你会得到一颗“如何路由日志”的逻辑树,而且每加一种输出方式就要改主流程代码。
用职责链改完后,每个输出器是一个独立节点,日志请求从链头开始走,每个节点各自判断“这个级别的日志我要不要管”。比如:
- Debug过滤器节点:只放行DEBUG级别的日志,自己处理不了就传给下一个。
- Error过滤器节点:拦截ERROR级别,写文件并上报监控。
- Remote上报节点:所有ERROR以上日志都通过HTTP上报。
链的好处是,你想临时关掉远程上报,只需要把那一个节点摘掉,不用动其他代码。
4.2 Web框架中的中间件管道
熟悉Web开发的朋友对“中间件”这个概念应该不陌生。Express、Koa、ASP.NET Core里都有中间件管道:请求进来先经过鉴权中间件,再经过日志中间件、参数校验中间件、路由处理中间件……每个中间件可以选择直接返回响应(短路),也可以调用next()把请求传给下一个中间件。
这本质上就是一个职责链模式的实践,只不过它的节点是“中间件”,请求是“HTTP请求”。你看,模式本身不绑定任何语言和框架,理解了职责链,你去看那些“高大上”的中间件原理,会突然觉得通透了很多。
C++世界里,如果你写过基于Boost.Beast或Drogon的HTTP服务,完全可以自己实现一个类似的中间件管道:维护一个std::vector<std::function<Response(Request, Next)>>,每个中间件拿到请求后决定是处理掉还是传给下一个。这种模式的扩展性极好——加一个新功能模块,就是往vector里塞一个新函数,老代码一行都不用改。
4.3 游戏开发中的事件系统
再说一个大家可能没那么熟悉但非常契合的场景:游戏里的输入事件处理。
一个UGUI界面或者一个3D场景里,鼠标点击、键盘按下、触摸手势,每个事件可能要经过UI界面、技能系统、移动系统、音效系统……每个系统都想先看一眼这个事件自己感不感兴趣,感兴趣就处理,不感兴趣就丢给下一个系统。用职责链实现,事件从链头传入,沿着系统列表依次传递,第一个“消费”掉事件的系统返回true,事件就不会再往后传。这跟Qt的事件过滤器、Unity的EventSystem执行顺序,本质上都是一路货色。
这一类需求最大的特点是:处理者的集合和顺序在配置阶段才确定,而且不同的场景可能需要不同的链。职责链模式把“场景级别的策略”从业务逻辑里抽离出来,变成了纯配置项,这就是它最大的实用价值。
5. 责任链的两个关键变体:短路式与全链式,以及如何选择
5.1 短路式(Classic Chain):一个节点处理完就停
这是GoF原书里的定义。请求沿链传递,第一个声称“我能处理”的节点负责处理,处理完就返回,后面的节点不再执行。审批、客服工单路由、故障定级都是这个模式。
优点:效率高,职责边界清晰,每个节点只需要关心自己关心的那一段。缺点:如果业务上需要“多个节点对同一请求依次处理”,短路式就做不到了——你不能指望组长批完以后,经理的“查看留痕”逻辑还会自动执行。
5.2 全链式(Pipeline / Filter):每个节点都处理一遍
全链式在业界还有一个更常见的名字:管道-过滤器模式(Pipeline and Filter)。请求经过整个管道,每个节点都对它做一次加工或记录,然后把加工后的结果传给下一个。日志输出、数据清洗流程、图像处理流水线,都是这种模式。
我见过很多人在讨论“职责链”时把这两种混为一谈,然后争论“到底哪个算职责链哪个不算”。说实话,这不是非此即彼的学术问题,你只要在设计你的链时明确说明自己是短路还是全链,并且让每个节点都按照同一个约定执行,就行了。代码可读性的最大敌人不是“用了哪种模式”,而是“调用者猜不到你的行为语义”。
从实现上看,两种模式的区别很小——短路式的节点处理完就return,全链式的节点处理完继续往后传。很多框架(比如ASP.NET Core中间件)甚至允许一个节点“先处理一部分,再决定要不要继续往下传”,那是两者的混合体,同样很实用。
5.3 怎么选:一张表看明白
| 对比维度 | 短路式 | 全链式 |
|---|---|---|
| 请求经过节点数 | 从链头到第一个能处理的节点 | 全部节点 |
| 典型场景 | 审批、路由、事件消费 | 日志、数据清洗、过滤器 |
| 节点关系 | 竞争关系(谁能处理谁上) | 协作关系(依次加工) |
| 返回值语义 | 返回是否已处理 | 返回处理后的数据或状态 |
| 性能特征 | 平均O(n/2),可能提前退出 | 稳定O(n),全程跑完 |
选型建议很简单:如果业务诉求是“找出那个合适的处理者”,用短路式;如果业务诉求是“所有人依次过一遍”,用全链式。这个判断几乎是第一反应,不需要纠结太多。
6. 绕不开的对比:职责链和装饰器,别再傻傻分不清
写设计模式的文章,如果不提职责链和装饰器的对比,总感觉缺了一块。这两个模式的结构很像——都是“对象持有下一个对象,调用层层向下传递”。面试里被问到“职责链和装饰器有什么区别”的概率,比你想的高得多。
结构上,它们确实很相似:
// 职责链 class Handler { std::shared_ptr<Handler> next_; void handle() { if (canHandle()) doHandle(); else next_->handle(); } }; // 装饰器 class Decorator { std::shared_ptr<Component> wrapped_; void operation() { before(); wrapped_->operation(); // 必须调用,不能跳过 after(); } };核心差别在于:装饰器倾向于增强,责任链倾向于分流。
装饰器模式的目标是在不改变原有接口的情况下,给对象动态增加新的行为。比如给一个文件流套上缓冲装饰器、加密装饰器、压缩装饰器——每个装饰器都是“在调用前后加点料”,但最终目的还是完成同一条操作链,调用链是不能断的。
职责链的目标则是避免请求发送者和接收者的耦合。链上的节点有“天生的惰性”——它可以选择拒绝处理,把请求往后传;也可以选择处理掉,让链条终止。节点之间的“责任边界”是动态划分的,并不是每个人都必须对请求动一刀。
一句话总结我自己的判断方式:如果每个节点都会执行自己的逻辑,并且大概率会调用下一个节点的核心操作,那是装饰器;如果每个节点都可能“撒手不管”或者“截胡处理”,那是职责链。
那什么情况下两者可以配合使用?有。比如HTTP中间件管道,每个中间件可能先做点前置处理(装饰器味道),然后决定是否短路返回(职责链味道)。实际框架里两者边界没那么泾渭分明,但理解原型的差别,能帮助你做设计决策时头脑更清醒。
7. 实战暗坑:我在职责链上踩过的四个问题
7.1 链的构建顺序:和直觉正好相反
很多新手第一次写职责链时,会写出这样的代码:
teamLeader->setNext(manager); manager->setNext(director);这个顺序看起来是“从链头开始依次往后连”,很自然。但如果你以后要动态构建链——比如从配置文件读取节点列表再构建——你会发现递归地倒着构建往往更省事:
// 从后往前构建,最后一个节点的next为nullptr std::shared_ptr<Approver> build(const std::vector<NodeConfig>& configs) { std::shared_ptr<Approver> head = nullptr; for (auto it = configs.rbegin(); it != configs.rend(); ++it) { auto node = createNode(*it); node->setNext(head); head = node; } return head; }倒序构建的核心优势是:你不需要维护“当前链尾”的指针,循环结束后的head自然就是链头。这个写法我之前多次遇到过,一开始总是用正序+tail指针,代码多好几行,后来改成倒序才觉得顺手。
7.2 性能陷阱:链过长时的递归栈深
经典的职责链实现用的是递归/虚函数调用:handle里判断不行就调next_->handle()。如果链比较长,比如有几十个节点,每一次请求都会递归压栈,极端情况下可能栈溢出。
场景举例:某规则引擎把几十条业务规则串成了一条链,每个请求进来都要从头跑到尾,高峰期QPS一高,监控里频繁看到stack-overflow。解决思路有几个:
- 改用循环遍历(就像我前面
std::function版本做的for循环),不递归就不会爆栈; - 如果必须保留节点类结构,可以在
handle里用while循环显式调用,而不是if (next) next->handle(order):
void handle(const Order& order) { Approver* current = this; while (current) { if (current->canApprove(order)) { current->approve(order); return; } current = current->next_.get(); } // 兜底 }这个写法既保留了链结构,又避免了递归调用的栈开销。
7.3 链中某一个节点抛异常,请求断在哪里
我在实际项目里踩过的最深的一个坑,是职责链中间节点抛异常后,整个请求状态变得不可预知。比如审批链中“组长审批成功并写库”,然后传给“经理审批”时,经理的远程接口超时抛异常——此时工单已经处于“一半被处理”的状态,上层捕获到异常后重试,又会出现“组长重复审批”。
这个问题不是职责链模式本身能解决的,它需要你在设计链节点时明确“异常边界”:
- 每个节点的处理动作尽量保证原子性——要么完全处理成功,要么完全失败;
- 节点之间的状态传递最好使用不可变对象,避免一个节点改了请求字段,后面节点抛异常之后数据回不滚;
- 如果确实需要跨节点事务,请引入补偿机制或把链的应用范围缩小,不要试图让职责链去解决分布式事务的问题。
职责链擅长的是“路由和分发”,不是“事务保障”。硬把事务性要求塞进职责链里,代码会变得非常拧巴。
7.4 “默认兜底节点”不是可有可无
文章前面我写过链尾的兜底分支,这里想再强调一下它的重要性。一个没有兜底逻辑的职责链,请求到了链尾没人处理,顶多是不执行任何代码就返回——表面上看起来“没出问题”,实际上业务上往往已经被悄然丢弃了。上线后如果有人抱怨“有些单子莫名其妙消失了”,大概率就是链尾没有兜底。
我现在的习惯是:每一条职责链必须有一个终结节点,要么是日志打印,要么是默认处理器,要么是异常抛出——反正不能什么都不做。甚至对于“什么都不做也是合理行为”的场景,也要写一行显式的日志“请求xxx未被任何节点处理”,这样才能保证排查问题时有迹可循。
8. 面试怎么聊:几个高频考察点与学习建议
职责链模式是C++面试中设计模式板块的高频话题,而且面试官经常把它往实战上引。整理几个我常见的考察方式:
其一,手写或口述职责链的核心结构。这时候不需要写完整的业务代码,把抽象基类、next指针、setNext、handle这几个关键点讲清楚就够了。最好顺带提一句你对生命周期管理的方案(比如shared_ptr),这能让面试官觉得你不只是背了书。
其二,聊聊职责链和if-else的取舍。这是个引战题。我的看法是:固定三五个分支、基本不会变的需求,用if-else完全没问题;一旦分支的增删变得频繁、或者分支顺序需要在运行时调整、或者多个业务方各自维护自己的处理逻辑时,if-else就会失控,这时候才值得引入职责链。面试官问这个问题的目的,往往不是考你“会不会用模式”,而是考你“会不会滥用模式”。
其三,问“如何测试一条职责链”。这个问题很多人答不好。职责链的测试重点是“链路行为”,而不是单个节点行为。单测每个节点当然要做,但更重要的是集成测试——构造几个典型的请求,验证它们分别停在了哪个节点、走了哪些路径。如果链支持动态配置,还要验证配置错误(比如节点不存在、形成环)时系统的表现。我在实际项目中会给每个链配置一个“全量路径测试用例表”,把每种典型请求的预期路径列出来,回归时直接跑一遍。
学习建议方面,我自己的路径是这样的:先看GoF原书职责链那一章,但别急着写代码;然后找自己正在维护的项目里有没有“一串if-else判断”的代码,尝试用职责链重构一个,对比重构前后的可读性和可维护性;最后把链的变体(短路、全链、混合)在自己的demo里各实现一遍,这样面试问起来才真有底气。
9. 最后分享一个我自己的习惯:先写“链的路径表”,再写节点类
写了很多次职责链之后,我养成了一个习惯,分享给大家。动工之前,不急着写代码,先在注释或文档里把这条链的“路径表”列出来。所谓路径表,就是回答这么几个问题:请求从哪来?可能经过哪些节点?哪些节点会终止链?哪些节点只看看不处理?链尾的兜底策略是什么?
比如审批链的路径表:
| 请求特征 | 首节点 | 可能路径 | 终止节点 |
|---|---|---|---|
| 金额<1000 | 组长 | 组长 | 组长 |
| 1000<=金额<5000 | 组长 | 组长;组长→经理 | 经理 |
| 5000<=金额<20000 | 组长 | 组长→经理;组长→经理→总监 | 总监 |
| 紧急工单 | 组长 | 组长→总监 | 总监 |
| 金额>=50000 | 组长 | 组长→经理→总监→财务抄送 | 总监 |
这张表的作用很大。它强迫你在写具体节点之前,把整条链的行为语义完整地想一遍——至少能发现几个边界case。而且这张表本身就是很好的代码注释,几个月后你回来看这段代码,打开表就明白当初为什么这么设计了。代码会骗人,需求文档会过期,但路径表描述的是系统运行时的真实动态,几乎不过时。
职责链模式本质上是个“组织行为”模式,它跟装饰器、策略模式相比并不炫技,却是我在实际工程中用得最顺手、重构收益最明显的模式之一。希望这篇文章能帮你把这条链,从书里搬到代码里。