news 2026/9/11 22:17:56

【设计模式精讲】29.行为型模式总结与对比(Behavioral Patterns Summary)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【设计模式精讲】29.行为型模式总结与对比(Behavioral Patterns Summary)

【设计模式精讲】29.行为型模式总结与对比(Behavioral Patterns Summary)

【摘要】:行为型模式常被压缩成「一堆回调技巧」,十一种模式背起来像十一道独立考题。其实按「各自要解决的问题」聚类,四个问题就装下了:一组对象怎么对话(观察者/中介者/责任链)、变化的行为装进哪里(策略/模板方法/状态)、过去怎么留住(命令/备忘录)、结构怎么加工(迭代器/访问者/解释器)。同组是近亲,结构最像、最易混淆,本文逐组细说区别与联系,重点辨析「通信三兄弟」;跨组模式几乎不会认错,却常串成流水线。再以横向对比表、决策树、组合案例、常见误判与评审清单收口。读完你能对着一段交互代码说出:问题属于哪一组,组内该选谁。
【关键词】:行为型模式、模式聚类、通信模型、职责分配、模式协作、决策树
【代码基准】:C++17

1. 十一种模式都在分配职责,为什么不能随便换

行为型模式常被压缩成一句话:「把行为做成对象。」这句话只描述了表面动作,没有说明每种模式把什么做成了对象、又要保护什么:责任链保护「发送者不知道谁会认领」,命令保护「请求留痕可回放」,备忘录保护「当时」那份数据的封装,观察者保护「发布者对订阅者的无知」,访问者保护「加操作不改结构」——十一种模式,十一种保护对象,这正是它们不能互相替换的原因。

把模式当成可互换的回调语法,设计很快走偏:用观察者做本该中介者做的事(事件之后需要按规则指挥多个对象,广播出去就失控了);用命令装策略(撤销栈里堆满被反复调用的算法对象);用备忘录扛命令的活(大状态深历史,内存先崩);把策略写成状态、状态写成策略(第 25/26 篇的镜像对互换);树形结构上用walk硬扛类型分派(第 28 篇早已给过升级路线)。细看这些走偏案例,几乎全是近亲冒充——用回答同一个问题的甲模式,去做乙模式的活;混组的错反而少见,没人会用迭代器去冒充命令。最容易认错的,永远是挤在同一个问题里的那几个模式,这正是下一节聚类的依据。

因此,不要先问「这里怎样加一层回调」,而要先问「压力属于哪一类问题」。若直接调用、直接返回、一个 if 就能表达,就保留简单设计。行为型模式比结构型更依赖意图判别——同一张类图,换个意图就是另一个模式,这是 25/26、19/26 两组「镜像对」反复证明过的。

2. 按问题聚类:四个问题装下十一种模式

十一个名字不必当十一个独立知识点背。按「要解决的问题」聚类,它们聚成四组:

共同的问题成员
通信一组对象之间,请求与事件怎么流动观察者、中介者、责任链
行为替换会变的行为装进哪里、由谁来换策略、模板方法、状态
历史回溯发生过的事怎么留、怎么回命令、备忘录
结构加工站在结构外面,按什么规则加工它迭代器、访问者、解释器

分组的第一个用处是收缩候选:认出问题属于哪一组,候选立刻从十一个缩到两三个。第二个用处是集中辨析:容易混淆的模式几乎都在组内,近亲们类图相似、术语相通,都有「接口 + 实现 + 委托」的骨架;跨组的模式形态差得远,几乎不会认错,反而经常合作——辨析的火力因此全部投向组内,下面逐组展开。

2.1 通信组:观察者、中介者、责任链

三者都在发送者与接收者之间架一条通道,让发送者不必指名道姓——这是行为型「解耦」的原始形态。区别全在对话的结构:

模式通信方向中间结构的意志终止性一句话
观察者一对多广播无:送达即结束不终止,各自反应「我变了,诸位自便」
中介者多对多经中心有:听报告、下指令中心裁决每次交互「都听我调度」
责任链一对一接力无:只有认领与否认领即停「不归我,交下一位」

判别只剩一问:事件之后谁做决定?没人做决定(各自反应)是观察者,一个中心做决定(指挥多方)是中介者,由认领者自己决定(处理即终点)是责任链。

联系比区别更值得记,三者常互相成全:中介者订阅同事的事件,是把观察者当通信底座(第 22 篇的回调槽装配正是此形);中介者把无人认领的请求转交专家链,是给责任链留外挂。观察者名单是「无序群发」,责任链是「有序、只出一个奖的抽奖」——给观察者名单加上顺序与短路语义,它就滑向责任链,GUI 事件冒泡正站在两者中间。

2.2 行为替换组:策略、模板方法、状态

三者都把「会变的行为」从使用方搬进独立单元,让不变的部分不必陪葬。区别在换什么、谁来换:

  • 策略换的是整个算法:由客户端注入(换不换是使用者的事),运行期可换,各策略是彼此陌生的平行解法。
  • 模板方法换的是骨架里的若干步骤:由子类填空,编译期定(注入版除外),骨架锁死在基类,子类是族成员。
  • 状态换的是随生涯阶段整套切换的行为:对象在操作里自己迁移(自己换自己),状态之间互为迁移目标、彼此认识。

两两对照是三场经典对决。策略 vs 模板方法是「委托 vs 继承」在行为型的正面清算,GoF 的比喻是一道菜的两种做法——整块替换还是磨碎成钩子;流程必须全族统一、只想定制个别步骤,用模板方法;连算法的内部结构都不想暴露给调用方,用策略。策略 vs 状态是全系列最著名的镜像对,两张 UML 几乎重叠,分野全在语义:谁驱动切换(外部注入 vs 自己迁移)、参与者是否互识(平行陌生 vs 互为迁移目标)、建模对象(算法插头 vs 对象生涯)。模板方法 vs 状态这对常被忽略:前者一次绑定,选定子类就定了填法;后者反复迁移,运行中随事件一换再换——前者固化流程,后者切换人格。

组内也常合作而非竞争:模板方法的钩子可以注入一个策略(第 27 篇注入版),状态的某个阶段内部也可以持一个策略——三者可以在不同层次各司其职。

2.3 历史回溯组:命令、备忘录

两者都在跟「过去」打交道:把现在发生的事保存成实体,让系统以后能撤销、回滚、重放、审计。区别在记什么:

  • 命令记操作——「做了什么」:动作、参数与逆操作随身携带;粒度细,可排队、可重放、可宏录制;撤销就是执行逆操作。
  • 备忘录记状态——「当时什么样」:一份密封快照,看守者只管保管不看内容;粒度粗,不关心操作序列;撤销就是恢复快照。

选择有现成口诀(第 23 篇三条路线):操作规整可逆用命令,状态小、操作杂用快照,深历史用「快照锚点 + 命令回放」的混合——数据库 WAL 与游戏 checkpoint 是同一配方的工业形态。两者也常合体:命令的undo()内部持一份操作前小快照,细账粗账一起记。

还要与邻组的策略划一条线:命令是一次请求的物化,生命周期与那次请求绑定、一次性消费;策略是长命算法,被反复调用、随时替换——「撤销栈里堆满算法对象」就是认错了这条线,第 6 节误判三正面展开。

2.4 结构加工组:迭代器、访问者、解释器

三者都站在对象结构外面加工它,把「怎么加工」从结构自己的类里搬出来——组合树不必为每种统计、导出、校验各长一个方法。区别在按什么规则加工:

  • 迭代器按位置:顺序交付元素、不问类型,客户端拿到元素后自己决定做什么。它交付的是「第几个」。
  • 访问者按类型:每个元素分派到对应的重载,一类元素一类动作、不问位置。它处理的是「是哪种」。
  • 解释器按语义:结构本身就是程序(文法树),「加工」就是执行它,变化的是句子而非文法。它执行的是「写了什么」。

这组内部与其说竞争,不如说流水线:迭代器找位置、访问者按类型分派(第 21/28 篇的「迭代 + 分派」);解释器与访问者是同一谱系的两端——「操作写在节点内」的 GoF 式解释器与「操作外置」的访问者在第 20 篇就已会师,而解释器的文法树本身就是组合模式(第 12 篇)。这组的赌注也最清楚:访问者赌元素类型封闭,解释器赌文法稳定,迭代器赌失效契约写得明白。

真实系统里四组同场:UI 框架中事件广播与控件联动是通信组,折扣与后端切换是行为替换组,操作历史是历史回溯组,渲染遍历是结构加工组——四组各答各的问题,互不抢戏;说不清压力落在哪一组,才是问题

3. 十一种模式横向对比表

模式分组意图核心角色典型所有权主要优点主要代价识别信号
观察者通信一对多依赖,变化自动通知Subject、Observer名单只借不拥,令牌或 weak 护生命周期运行时增删订阅,广播免轮询通知风暴、顺序无保证、生命周期三坑一对多、订阅者动态、主题不问后果
中介者通信封装一组对象的交互Mediator、Colleague中介者创建并持有同事网状收星型,规则集中可单测中枢易成上帝对象,单点高扇入一组对等对象按复杂规则互相联动
责任链通信请求沿链传递,直到有对象处理Handler、ConcreteHandler、Client链节点常不拥有后继,装配方管理发送者与处理者解耦,增删处理者只动装配请求可能无人认领,长链空转,认领隐式难调试多个候选者按序给机会、通常一个认领
策略行为替换算法族各自封装可互换Strategy、ConcreteStrategy、Context策略自带配置,注入后借用或持有运行时换算法,消除条件语句选择负担,接口分母过肥平行解法要增、要换、要组合
模板方法行为替换骨架固化,步骤留给子类AbstractClass、ConcreteClass骨架与横切逻辑归基类仪式只写一次,扩展点可枚举继承硬耦合,骨架演化伤全员一族流程顺序相同、个别步骤不同
状态行为替换状态一变、行为整套换State、ConcreteState、Context状态对象零数据全共享,字段归上下文switch 消除,迁移出边局部化类数随状态膨胀,全景图要拼装行为随阶段显著变化、状态较多
命令历史回溯把请求封装为对象Command、Invoker、Receiver、Client命令入队后归调用者,自足携带接收者排队、日志、撤销、宏、序列化全白拿类数量膨胀,不可逆操作的 undo 是真功夫请求要留痕、重放、撤销或跨线程投递
备忘录历史回溯封装内化对象状态供恢复Originator、Memento、Caretaker快照归看守者保管,解释权归原发器封装不破,状态恢复集中,快照可搬运全量快照内存大,含句柄状态要立约撤销、事务、存档、回溯
迭代器结构加工顺序访问聚合而不暴露表示Iterator、Aggregate、Client迭代器是未托管句柄,容器定义失效契约遍历与表示解耦,接入 STL 上百算法失效规则是文档契约,样板重自定义容器、多遍历序、惰性流式数据
访问者结构加工不改元素类为结构加操作Visitor、Element、ObjectStructure操作及其状态归访问者,遍历常归访问者加操作零改动结构,加类型编译点名加元素全访问者返工,循环依赖结构稳定、操作持续疯长
解释器结构加工给文法一个表示和解释器Expression、Terminal、Nonterminal、Context树归解析产物,非终结符拥有子树规则进数据,句子可变而文法稳定类爆炸,解析报错版本兼容是隐形工程量规则以字符串到达、文法小而稳

这张表不是关键词匹配器。「有回调」不能证明观察者,「有 switch」不能证明策略或状态,「有个中间类」不能证明中介者。反向核验的永远是角色与所有权:谁持有谁、谁决定谁、状态与历史归谁、失败与终止谁负责。

4. 选择决策树

下面的决策树按第 2 节的分组逐层过滤,四层就是四个问题:

没有

广播出去,各自反应

需要中心指挥多方

多个候选者按序给机会

整个算法要可换

骨架固定只差步骤

行为随状态整体切换

请求要留痕、撤销、回放

状态要存档回滚

统一遍历不问表示

稳定结构加新操作

字符串规则要执行

都不是

交互代码出现了明确的职责分配压力吗

不需要模式:直接调用与返回

通信:一组对象怎么对话

观察者

中介者

责任链

行为替换:要换的是行为本身吗

策略

模板方法或注入

状态;两态用 enum

历史回溯:需要回到过去吗

命令;深历史混第 23 篇快照

备忘录

结构加工:要在结构上做文章吗

迭代器

访问者

解释器或嵌入引擎

四层的顺序不是重要性排序:先问通信,因为通信结构影响全局拓扑,改错最疼;再问行为替换,它牵动调用方与实现方的接口关系;再问历史回溯,撤销与状态机的错误一旦定型返工最贵;最后是结构加工,它们最像库代码,替换成本最低。走到「不需要模式」依然是理想结果——一个 if 能分清的两个候选者、一个从不撤销的操作、一个只有两个订阅者的通知,直接写就是最好的行为型模式。

5. 模式怎样组合

分组解决「选谁」,组合解决「怎么一起用」。同组的搭档靠分工:一个做底座一个做大脑、一个记细账一个记粗账;跨组的搭档靠流水线:各管一段,互不知情。

命令 + 备忘录(同组搭档)是撤销的完整答案:命令记操作与参数,快照定锚点,深历史下「回滚到锚点 + 重放」;第 19、23 篇各自收尾时都指向这条混合路线,数据库 WAL 与游戏 checkpoint 是同一配方。

观察者 + 中介者(同组分层)是事件系统的标准分层:同事发事件(观察者底座),中枢订阅并按规则指挥(中介者大脑)——第 22 篇改进一的回调槽装配正是它。

组合 + 访问者 + 迭代器(跨族流水线)是结构加工的完整链路:组合提供树,迭代器按位置交付,访问者按类型分派——第 12、21、28 篇三笔账在 AST 上一次结清。

下面的小例子跨三组串成一条告警流水线:传感器样本经观察者总线广播(通信),策略判定是否越限(行为替换),越限动作物化为命令进异步队列(历史回溯):

#include<functional>#include<memory>#include<queue>#include<string>#include<vector>// ---- 观察者:样本总线(第 24 篇槽位版)----classSampleBus{public:usingSlot=std::function<void(double)>;usingId=int;Idsubscribe(Slot s){slots_.emplace_back(next_,std::move(s));returnnext_++;}voidpublish(doublev){autosnap=slots_;// 副本抗重入for(auto&[id,s]:snap)if(s)s(v);}private:std::vector<std::pair<Id,Slot>>slots_;intnext_=0;};// ---- 策略:越限判定(第 26 篇)----usingAlarmRule=std::function<bool(double)>;AlarmRuleoverThreshold(doublelimit){return[limit](doublev){returnv>limit;};}// ---- 命令:告警动作(第 19 篇)----structAlertAction{std::function<void()>execute;std::string reason;// 可审计的凭据};classAlertQueue{public:voidpush(AlertAction a){pending_.push(std::make_unique<AlertAction>(std::move(a)));}voiddrainAll(){while(!pending_.empty()){pending_.front()->execute();pending_.pop();}}private:std::queue<std::unique_ptr<AlertAction>>pending_;};// ---- 装配:三种模式在组合根相遇 ----classMonitor{public:explicitMonitor(SampleBus&bus):bus_(bus){}voidwireUp(AlarmRule rule,AlertQueue&alerts){rule_=std::move(rule);queue_=&alerts;sub_=bus_.subscribe([this](doublev){onSample(v);});}private:voidonSample(doublev){if(rule_(v)){std::string why="value "+std::to_string(v)+" over limit";queue_->push({[why]{/* 发短信/写库/呼号 */},why});}}SampleBus&bus_;AlarmRule rule_;AlertQueue*queue_=nullptr;SampleBus::Id sub_=0;};intmain(){SampleBus bus;AlertQueue alerts;Monitormon(bus);mon.wireUp(overThreshold(100.0),alerts);bus.publish(42.0);// 判定未过,bus.publish(131.4);// 命令入队alerts.drainAll();// 后台批量执行return0;}

三种模式各守边界:总线不知道谁在听(观察者),规则是一段可替换的可调用体(策略),动作是带凭据的对象、进队后随取随放(命令)。想换「滑动窗口判定」只换AlarmRule;想加「告警落盘」再订一个总线槽;想把告警改成「合并五分钟一条」只动AlertQueue——三个问题各有自己的落点,这是行为型组合的全部意义

6. 常见误判

回看高频误判,认错都发生在近亲之间——形态差得远的模式,没人会认错。

误判一:「加了个回调」就叫观察者。观察者的成立要件是可增减的订阅名单发布者对订阅者的无知。单一固定回调是依赖注入或策略槽;写死的两个listener->on()调用只是间接层。反之,明明有五方关心事件、名单要运行时增减,还硬用字段回调,就是欠下的观察者。

误判二:状态与策略傻傻分不清。三问定案:谁驱动切换(外部注入 vs 自己迁移)、参与者是否互识(平行陌生 vs 互为迁移目标)、建模对象(算法 vs 生涯)。把策略写成状态,算法对象之间会冒出不该有的迁移耦合;把状态写成策略,迁移规则会散回调用方、switch 借尸还魂。

误判三:命令当策略、策略当命令。一次请求(入栈、撤销、回放、跨线程投递)用命令;长命算法(反复调用、随时替换、作为库的定制点)用策略。撤销栈里堆算法对象、或给每个函数调用包一层命令类,都是两头不讨好。

误判四:结构不稳定就上访问者。访问者的赌注是「元素类型封闭」。类型清单还会长的(配置模型、协议版本演进),用variant保持编译期显式,或干脆 switch 加单测兜底;硬上访问者,每次加类型都是全族访问者的返工日。反过来,操作只有一两个也犯不上双分派——直接成员函数最便宜。

还要警惕把「接口有多个实现」叫策略,「有个 switch」叫状态机,「有事件」叫发布订阅。行为型的识别永远看角色与所有权:谁决定、谁认领、谁保管历史、谁负责终止——答不清这些,模式名称再精确也没有意义。

7. 代码评审清单与下一部分

评审交互代码时,可以依次检查以下问题:

  • 问题定位:压力属于通信、行为替换、历史回溯还是结构加工?组内若同时出现两个模式,分工(底座/大脑、细账/粗账)说得出吗?
  • 谁决定:事件之后谁做决定——各自反应、中心指挥、认领即停?通信结构与业务语义一致吗?
  • 时间维度:请求需要留痕或撤销吗?状态需要存档吗?撤销走命令、快照还是混合?「无人认领」「恢复失败」的兜底在哪里?
  • 生命周期:回调对象可能先死吗?订阅有令牌或 weak 护体吗?notify 中的增删被重入护栏挡住了吗?
  • 顺序依赖:观察者之间、过滤器之间是否藏着隐式顺序假设?顺序变化时谁来暴露这个假设?
  • 过度设计:单实现配了策略接口、两状态配了状态类、单订阅配了总线、从不撤销配了命令栈——哪个抽象的扩展点从未被使用?

十一种行为型模式最终都在回答「职责怎样分配」。分组把选择变成两步:先认出问题属于哪一组,再在组内两三个近亲里挑出最小的机制;需要组合时,让每种模式只守住自己的边界。

下一部分是全专栏的收官:把创建、结构、行为三族 23 种模式放到一个真实小项目里串起来,展示它们如何协作而非堆砌;然后是避坑指南与反模式速览——包括那个贯穿全系列的追问:什么时候不该用模式。

本文的模式定义与取舍参考了 Refactoring Guru《设计模式》中文版相应各章,分组归纳与后果清单参考了 GoF《Design Patterns》第 5 章各模式小节,并综合了本系列第 18–28 篇的工程化表述。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 22:17:18

OpenCvSharp实现USM锐化:C# WinForm图像清晰化实战

简介&#xff1a;面向C#开发者的OpenCvSharp图像锐化演示工程&#xff0c;演示了在WinForm中实现USM锐化的完整流程&#xff0c;可有效提升图片清晰度与边缘细节。压缩包共35个文件&#xff0c;总大小约69MB&#xff0c;包含可直接运行的exe、C#源程序、OpenCvSharp依赖DLL、XM…

作者头像 李华
网站建设 2026/9/11 22:15:39

Flutter组件在鸿蒙平台的dart_scope迁移实践

1. 项目背景与核心挑战在跨平台开发领域&#xff0c;Flutter和鸿蒙HarmonyOS代表着两种截然不同的技术路线。当我们需要将成熟的Flutter组件迁移到鸿蒙平台时&#xff0c;dart_scope这个负责作用域治理的组件遇到了几个关键挑战&#xff1a;生命周期模型差异&#xff1a;Flutte…

作者头像 李华
网站建设 2026/9/11 22:15:08

华为硬件电源岗真题解析:28V电源切换电路设计实战

1. 这不是“题库搬运”&#xff0c;而是电源工程师的实战能力切片如果你在搜“华为2026校招硬件电源岗真题答案”&#xff0c;大概率正卡在三个现实困境里&#xff1a;一是手头只有零散题目片段&#xff0c;没上下文、没评分标准、更没解题逻辑链&#xff1b;二是刷遍了网上所谓…

作者头像 李华
网站建设 2026/9/11 22:11:06

image2.5 突临,平面世界被彻底抹平

预计字数&#xff1a;约 5000 字 阅读约 15 分钟 难度等级&#xff1a;⭐&#xff08;零门槛&#xff0c;实测判断文&#xff09; 核心价值&#xff1a;20 组同题对比实测 GPT-Image-2.5 和 2.0 的真实差距&#xff0c;说清"平面被抹平"到底抹掉了什么、没抹掉什么…

作者头像 李华
网站建设 2026/9/11 22:09:16

空圈 从科幻到工程:用“双重降维移植”把曲速引擎变成4套可落地的科研预研方案(附 C-M-P 损失函数)

本文由作者与 AI&#xff08;元宝/盼盼&#xff09;历时两个多月的对话中协作推演完成&#xff0c;经豆包、通义、DeepSeek 多轮交叉验证后形成。 核心驱动力是对“边界与自限”的坚持。 本文档为科研白皮书草稿&#xff0c;非成品结论&#xff0c;所有框架推演均标注不确定性。…

作者头像 李华
网站建设 2026/9/11 22:08:50

OpenClaw安全隔离:Firecracker沙箱技术实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华