【设计模式精讲】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 节的分组逐层过滤,四层就是四个问题:
#mermaid-svg-Gn7GwOcidZegAhPN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Gn7GwOcidZegAhPN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Gn7GwOcidZegAhPN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Gn7GwOcidZegAhPN .error-icon{fill:#552222;}#mermaid-svg-Gn7GwOcidZegAhPN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Gn7GwOcidZegAhPN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Gn7GwOcidZegAhPN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Gn7GwOcidZegAhPN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Gn7GwOcidZegAhPN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Gn7GwOcidZegAhPN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Gn7GwOcidZegAhPN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Gn7GwOcidZegAhPN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Gn7GwOcidZegAhPN .marker.cross{stroke:#333333;}#mermaid-svg-Gn7GwOcidZegAhPN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Gn7GwOcidZegAhPN p{margin:0;}#mermaid-svg-Gn7GwOcidZegAhPN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Gn7GwOcidZegAhPN .cluster-label text{fill:#333;}#mermaid-svg-Gn7GwOcidZegAhPN .cluster-label span{color:#333;}#mermaid-svg-Gn7GwOcidZegAhPN .cluster-label span p{background-color:transparent;}#mermaid-svg-Gn7GwOcidZegAhPN .label text,#mermaid-svg-Gn7GwOcidZegAhPN span{fill:#333;color:#333;}#mermaid-svg-Gn7GwOcidZegAhPN .node rect,#mermaid-svg-Gn7GwOcidZegAhPN .node circle,#mermaid-svg-Gn7GwOcidZegAhPN .node ellipse,#mermaid-svg-Gn7GwOcidZegAhPN .node polygon,#mermaid-svg-Gn7GwOcidZegAhPN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Gn7GwOcidZegAhPN .rough-node .label text,#mermaid-svg-Gn7GwOcidZegAhPN .node .label text,#mermaid-svg-Gn7GwOcidZegAhPN .image-shape .label,#mermaid-svg-Gn7GwOcidZegAhPN .icon-shape .label{text-anchor:middle;}#mermaid-svg-Gn7GwOcidZegAhPN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Gn7GwOcidZegAhPN .rough-node .label,#mermaid-svg-Gn7GwOcidZegAhPN .node .label,#mermaid-svg-Gn7GwOcidZegAhPN .image-shape .label,#mermaid-svg-Gn7GwOcidZegAhPN .icon-shape .label{text-align:center;}#mermaid-svg-Gn7GwOcidZegAhPN .node.clickable{cursor:pointer;}#mermaid-svg-Gn7GwOcidZegAhPN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Gn7GwOcidZegAhPN .arrowheadPath{fill:#333333;}#mermaid-svg-Gn7GwOcidZegAhPN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Gn7GwOcidZegAhPN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Gn7GwOcidZegAhPN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gn7GwOcidZegAhPN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Gn7GwOcidZegAhPN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gn7GwOcidZegAhPN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Gn7GwOcidZegAhPN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Gn7GwOcidZegAhPN .cluster text{fill:#333;}#mermaid-svg-Gn7GwOcidZegAhPN .cluster span{color:#333;}#mermaid-svg-Gn7GwOcidZegAhPN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Gn7GwOcidZegAhPN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Gn7GwOcidZegAhPN rect.text{fill:none;stroke-width:0;}#mermaid-svg-Gn7GwOcidZegAhPN .icon-shape,#mermaid-svg-Gn7GwOcidZegAhPN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gn7GwOcidZegAhPN .icon-shape p,#mermaid-svg-Gn7GwOcidZegAhPN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Gn7GwOcidZegAhPN .icon-shape .label rect,#mermaid-svg-Gn7GwOcidZegAhPN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gn7GwOcidZegAhPN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Gn7GwOcidZegAhPN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Gn7GwOcidZegAhPN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 没有

广播出去,各自反应
需要中心指挥多方
多个候选者按序给机会

整个算法要可换
骨架固定只差步骤
行为随状态整体切换

请求要留痕、撤销、回放
状态要存档回滚

统一遍历不问表示
稳定结构加新操作
字符串规则要执行
都不是
交互代码出现了明确的职责分配压力吗
不需要模式:直接调用与返回
通信:一组对象怎么对话
观察者
中介者
责任链
行为替换:要换的是行为本身吗
策略
模板方法或注入
状态;两态用 enum
历史回溯:需要回到过去吗
命令;深历史混第 23 篇快照
备忘录
结构加工:要在结构上做文章吗
迭代器
访问者
解释器或嵌入引擎

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

5. 模式怎样组合

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

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

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

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

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

cpp 复制代码
#include <functional>
#include <memory>
#include <queue>
#include <string>
#include <vector>

// ---- 观察者:样本总线(第 24 篇槽位版)----
class SampleBus {
public:
  using Slot =
      std::function<void(double)>;
  using Id = int;

  Id subscribe(Slot s) {
    slots_.emplace_back(next_,
                        std::move(s));
    return next_++;
  }

  void publish(double v) {
    auto snap = slots_;   // 副本抗重入
    for (auto& [id, s] : snap)
      if (s)
        s(v);
  }

private:
  std::vector<std::pair<Id, Slot>>
      slots_;
  int next_ = 0;
};

// ---- 策略:越限判定(第 26 篇)----
using AlarmRule =
    std::function<bool(double)>;

AlarmRule overThreshold(
    double limit) {
  return [limit](double v) {
    return v > limit;
  };
}

// ---- 命令:告警动作(第 19 篇)----
struct AlertAction {
  std::function<void()> execute;
  std::string reason;   // 可审计的凭据
};

class AlertQueue {
public:
  void push(AlertAction a) {
    pending_.push(
        std::make_unique<
            AlertAction>(
            std::move(a)));
  }

  void drainAll() {
    while (!pending_.empty()) {
      pending_.front()
          ->execute();
      pending_.pop();
    }
  }

private:
  std::queue<
      std::unique_ptr<AlertAction>>
      pending_;
};

// ---- 装配:三种模式在组合根相遇 ----
class Monitor {
public:
  explicit Monitor(SampleBus& bus)
      : bus_(bus) {}

  void wireUp(AlarmRule rule,
              AlertQueue& alerts) {
    rule_ = std::move(rule);
    queue_ = &alerts;
    sub_ = bus_.subscribe(
        [this](double v) {
          onSample(v);
        });
  }

private:
  void onSample(double v) {
    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;
};

int main() {
  SampleBus bus;
  AlertQueue alerts;

  Monitor mon(bus);
  mon.wireUp(overThreshold(100.0),
             alerts);

  bus.publish(42.0);    // 判定未过,
  bus.publish(131.4);   // 命令入队
  alerts.drainAll();    // 后台批量执行
  return 0;
}

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

6. 常见误判

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

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

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

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

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

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

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

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

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

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

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

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

相关推荐
啦啦啦啦啦zzzz2 小时前
Logger的组装(c++20)
c++·算法·c++20
繁星蓝雨4 小时前
C++的设计与演进大纲(如何从C语言一步步成长为C++)
c语言·开发语言·c++·c++历史·c++演进
蒸蒸yyyyzwd4 小时前
机试学习笔记
c++·求职招聘
geovindu5 小时前
CSharp: Observer Pattern
开发语言·后端·观察者模式·设计模式·c#·.netcore·行为模式
handler015 小时前
【Linux】线程与虚拟内存的底层世界
linux·运维·服务器·c++·线程·虚拟地址空间·共享内存
郝学胜-神的一滴5 小时前
C++11 工程级应用 09:告别无谓拷贝,解锁高性能移动语义
开发语言·数据结构·c++·vscode·软件工程·visual studio
hetao17338377 小时前
校内场-提高组 ZYZSC-S-Round 4
c++·算法
stellanke8 小时前
Linux网络编程实战2:TCP Socket 基础与实践
linux·网络·c++
蒸蒸yyyyzwd8 小时前
机试和Redis学习笔记
c++·笔记·面试