【设计模式精讲】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 篇的工程化表述。