想象一家生意火爆的餐厅:后厨有川菜、粤菜、西餐、甜品四位师傅;服务员端着托盘穿梭;大堂经理在前厅八方调度。怎样才能让这支队伍高效配合,又不互相干扰?答案是——每个人只管好自己的事,同时遵循一套清晰的“协作规则”。顾客说想吃辣,厨师先问“微辣还是特辣”;后厨出餐时广播喊一句“十三号桌的菜好了”,所有需要知道的人自动响应;顾客要求退菜,服务员按流程执行,不必清楚后厨如何改菜。 软件世界里的对象,就像餐厅里的角色。行为型模式(Behavioral Patterns)就是一套编排对象间“协作规则”的智慧结晶:它关注职责分配、通信方式和控制流转移,让对象既能独立演化,又能默契配合。策略的切换、状态的流转、请求的层层传递,背后都有宝藏可挖。本文就用这家餐厅的日常运转,带你读懂行为型模式的八大核心招法。
一句话职业价值:行为型模式是搭建可扩展、可维护 AI 系统的底层思维——训练流程编排、多模型调度、Agent 协作都离不开这些协作套路。
目录
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
策略模式:菜单上可换的招牌菜
顾客在餐厅点“宫保鸡丁”,菜单上写着“辣度可选:微辣/中辣/特辣”。后厨不用更换整套烹饪流程——锅还是那口锅,料还是那些料,只是换掉“辣度方案”。这就是策略模式最朴素的场景:同一个任务,由调用者选择一种算法来执行。
软件世界里,“同一道菜”往往对应“同一个业务接口”:排序可以用快排也可以用归并;AI 推理可以用 GPU 满精度,也可以切到 CPU 量化。策略模式(Strategy Pattern)把一族算法各自封装成独立的“策略类”,让上下文(Context)通过统一接口调用。所谓“耦合”,就是两个模块纠缠在一起难以分离;策略模式的目的是把“用哪个算法”和“怎么用”彻底拆开。
策略模式包含三个角色:上下文持有策略引用,负责调用;策略接口定义统一方法;具体策略类实现各自的算法细节。调用方在运行时可以动态替换策略——就像顾客临时改口“换特辣”,后厨只需换一个调料方案。用公式表达:
策略模式的可替换性,正如后厨换辣度方案而不动灶台——上下文在运行时调用同一接口,却能在不同算法间平滑切换。理解这点之后,你会发现 AI 里的“推理后端选择”本质上也是一道策略选择题。
- 策略集合 S所有可替换算法构成的集合,对应后厨的“辣度方案表”:
- 代价函数 Cost衡量某个策略在当前输入 $x$ 下表现好坏的标准,相当于顾客对“辣度指数”的权衡:
- argmin 运算从集合中挑出代价最小的那一项,而不是返回代价本身:
- 最优策略 s\*被选中的策略对象,最终被上下文调用执行:
观察者模式:广播声响起,各岗位自动响应
餐厅后厨有一个传菜口,菜好了就对着广播喊:“十三号桌的菜好了!”服务员听到了去上菜,外卖员听到了去打包,领位员听到了知道这桌快翻台了。没有人去逐一询问“你要不要这道菜的消息”——信息自己扩散到所有关心的人那里。
观察者模式正是这样一种“一对多”的通知机制。主题(Subject)维护一份观察者列表,状态变化时调用 notify() 广播事件;观察者(Observer)注册后,就会在主题变化时自动收到更新。模型训练时,指标变化要同时通知日志、可视化面板和告警机器人——这就是观察者模式的典型战场。
主题不需要知道观察者是谁,观察者也不需要主动轮询,双方只依赖抽象的订阅关系。这种松耦合让系统像广播一样自然:新加一个观察者,只需注册;移除一个,只需注销。通知过程可以形式化为:
后厨的广播不需要知道谁在听,只要有心人,就会听到。观察者模式让对象之间形成“发消息不用点名”的自然协作关系,这正是事件驱动架构的基石。
update;所有更新动作并在一起,就完成了事件 $e$ 的广播。- 观察者集合 O所有注册了“收听”的对象,相当于听到广播后行动起来的人员:
- update 方法每个观察者收到事件后的响应逻辑,类似服务员听到菜好后去端菜:
- 并集运算对所有观察者逐个触发更新,不遗漏任何一个注册者:
- 广播结果一次
notify调用就能把事件送达所有关心它的对象:
命令模式:一张小票把请求变成对象
顾客向服务员说“来一份宫保鸡丁,微辣”,服务员不会把这句话原样口传给厨师,而是写下一张小票:菜名、辣度、桌号。后厨照着执行,叫号时凭票取餐。这张小票,就是把“请求”变成实在物体的过程。
命令模式(Command Pattern)把一次方法调用封装成独立的命令对象。调用者(Invoker)只负责在合适时机触发 execute();命令对象内部保存接收者(Receiver)和必要参数;接收者真正动手干活。小票能排队、能改单、能退菜,命令对象同样支持延迟执行、日志记录、撤销重做。
当你点“撤销”时,看似是系统的神奇能力,其实是命令对象保存了逆操作 undo()——就像小票注明“可退”,后厨照做。命令模式让请求具备“物质形态”,因此可以在系统里排队、存档、回放:
小票把顾客的嘴巴和厨师的手解耦——顾客不必进厨房,厨师不必听口述。命令模式让请求脱离发送者独立存在,从而解锁排队、撤销、日志等一系列能力,这是 AI 任务调度系统中极其常见的套路。
execute(),调用者触发它后得到结果 $R$。小票上写的东西越清楚,后厨执行越可靠;命令对象封装得越好,系统越灵活。- 调用者 Invoker持有命令对象的引用,并在合适的时机触发命令,像服务员接过小票后交给后厨:
- 命令对象 Command包含接收者引用和执行所需参数,核心方法就是执行动作:
- 接收者 Receiver真正执行业务逻辑的对象,像后厨的师傅照着小票做菜:
- 执行结果 R命令执行完毕返回的结果,可能是布尔值、数据或状态变更:
迭代器模式:服务员按顺序上菜
服务员给一桌上菜,不需要知道后厨有几口锅、冰箱在哪个角落,她只需要知道:这一桌还剩几道菜,下一道该端什么。顺序,就是她对“上菜”这个集合的唯一认知。
迭代器模式(Iterator Pattern)让你以同样方式遍历一个集合:不需要知道底层是数组、链表还是树,只要拿到一个游标,依次问道“还有吗?给我下一个”。hasNext() 判断是否还有元素,next() 返回当前元素并移动游标。
集合暴露的越少,内部越容易变化。今天用数组存储,明天改成链表,只要迭代器接口不变,调用方代码一行都不用改。这就像服务员只认“几号桌、第几道菜”,不管后厨用什么灶:
服务员无须进入厨房,迭代器无须暴露内部结构——遍历逻辑和存储结构彻底分离,这在处理大规模数据集(如训练批次取样)时至关重要。
- hasNext 判断检查集合中是否还有未遍历的元素,像服务员看了一眼托盘还有几道菜:
- next 取元素返回游标当前指向的元素,并把游标后移一位:
- 元素序列被遍历的底层数据容器,对迭代器而言只是一个黑盒:
- 循环条件
hasNext()为真时才继续取下一个,保证不会越界:
状态模式:订单跳动的生命周期
一张订单在餐厅里的命运清晰而严格:待接单 → 制作中 → 已出餐。待接单时,顾客可以取消;制作中就不能随便取消;已出餐后,连改口味都不行。每个状态有自己允许的操作,流程本身像一个会自动跳转的装置。
如果在代码里用一堆 if 判断“当前是哪个状态”,状态一多,代码里到处是条件分支,改一个状态就要动所有地方。状态模式(State Pattern)把每个状态封装成独立的状态类,上下文对象保存一个“当前状态对象”,事件到来时委托给它处理;状态类内部还可以决定流转到哪个下一状态。
订单状态的流转,本质上是一个状态转移函数:给定当前状态和触发事件,推出下一个状态。这像极了餐厅运转规则——状态对象就是“规则卡片”:
订单就像一个有生命的个体,每个阶段都由“规则”自动接管。状态模式把杂乱的条件判断重构成清晰的状态类集合,在 AI Agent 的多阶段任务流程中也大有用武之地。
- 状态集合 S对象可能处于的全部状态,对应订单的三个阶段:
- 触发事件 E能让状态发生变化的输入信号,像“下单”“开始制作”“出餐”:
- 转移函数 δ输入当前状态和事件,输出下一状态,是状态机的核心规则:
- 状态更新上下文把内部状态替换为转移结果,行为随之改变:
模板方法模式:标准作业流程
餐厅后厨有一份《标准作业流程》:备料 → 开火 → 下锅 → 翻炒 → 装盘。不管是川菜师傅还是粤菜师傅,都必须按这个骨架走;不同的是,下锅后的具体手法各有高招。菜单换了,骨架却不换——这就是流程复用的力量。
模板方法模式(Template Method Pattern)在父类中定义一个算法的骨架,把某些具体步骤延迟给子类实现。父类的模板方法负责按固定顺序调用步骤;子类只需实现自己关心的“变化点”,保证整体流程一致。
除了抽象步骤,模板方法还提供钩子方法(hook method):父类里有一个默认实现或空实现,子类可以选择覆盖它,插入额外逻辑而不破坏流程。这就像 SOP 允许师傅在装盘前撒一把自己的秘制葱花:
SOP 没有教厨师如何为每道菜调味,却保证了出餐流程一致——模板方法模式同样如此。在 AI 领域,标准的训练流水线、推理流水线都可以用模板方法固化骨架,把“换模型头”“换损失函数”留作子类钩子。
- 流程步骤链算法骨架由一串按顺序执行的步骤组成:
- 抽象步骤部分步骤在父类中只声明不实现,等待子类填充:
- 钩子方法子类可选的覆盖点,通常有默认空实现:
- 模板执行骨架 + 子类细节组合成完整算法,保证可复用与可扩展:
责任链模式:顾客投诉的逐级上报
顾客不满意了,先向服务员提。服务员解决不了,上报领班;领班拿不准,请经理出马。这条“投诉链”上,每个节点都有机会处理,处理不了就往上传。顾客不需要知道最终是谁解决的,他只需要把不满说出来。
责任链模式(Chain of Responsibility Pattern)把多个处理者串成一条链,请求沿着链依次传递,每个节点自主决定“我处理”还是“传给下一个”。发送者与接收者彻底解耦——发送者不知道也不关心链上有谁。
这给系统带来极大的动态性:可以随时增删处理节点、调整顺序。比如 AI 内容审核流程:长度检查 → 敏感词过滤 → 语义审核 → 人工复审,任何一个环节拦截住,请求就终止。假设每个节点独立处理成功的概率为 $p_i$,则整条链至少有一个节点处理成功的概率是:
每一环只问一句“这事我管不管?”,不管就往下传——责任链让请求在链上流动,让责任在各环间分散,也让 AI 流水线中的前置检查逻辑变得轻盈可插拔。
- 单节点成功率第 $i$ 个节点独立处理请求的概率,代表这一级“接得住”的能力:
- 全部失败概率每个节点都处理不了,请求才无人接住:
- 补集运算用 1 减去“全部失败”,得到至少一个节点成功的概率:
- 工程含义链越长,请求被处理的概率越高,但每个节点都要消耗时间,所以要在覆盖率与延迟之间权衡:
中介者模式:大堂经理的调度中枢
如果餐厅没有大堂经理,服务员要自己去找后厨、收银、仓管、外卖骑手逐一沟通:菜要催、单要改、餐具要补……关系乱成一团麻。有了大堂经理,一切协调都找他:他统一调配、传话、决策,大家只需要和他沟通。
中介者模式(Mediator Pattern)用一个中介对象封装一组对象的交互,把网状结构变成星型结构。每个同事对象只持有中介者的引用,消息发给中介者,由它转给目标对象。这样对象之间互不知道彼此的存在。
交互逻辑被集中到中介者身上,修改协作规则时只改中介者,不用逐个改动同事类。代价是中介者本身可能变得复杂,所以要把控交互规则的边界。通信路径可以简洁表示为:
星型的沟通结构,正是中介者模式的核心:协作规则的复杂度收拢到一处,其他人只需守好自己的岗位。在多智能体协作系统中,中介者模式经常扮演“总控 Agent”的角色。
- 同事对象集合原本需要互相通信的一组协作对象:
- 中介者 M集中所有交互逻辑的核心对象,扮演调度者角色:
- 通信路径任何两个同事之间的消息都换成间接传递:
- 链路数量对比直接通信需 $n \times (n-1)$ 条链路,星型结构只需 $n$ 条:
AI 应用场景
动态推理策略切换(策略模式)
阶段:模型部署与推理优化
解决的问题:传统的单一推理引擎难以兼顾不同输入——短文本用 GPU 跑浪费资源,长文本 CPU 又太慢。用策略模式将“高精度 GPU 推理”“CPU 快速推理”“量化加速推理”封装为策略类,推理服务器根据请求长度、延迟预算动态切换。
具体案例:某 OCR 在线服务在晚高峰流量激增时,自动将低置信度优先策略切换为“批量优先”策略;同一份图片,不同时段走不同推理策略,成本降低约 35%。
训练指标广播监控(观察者模式)
阶段:模型训练与监控
解决的问题:训练过程中 loss、accuracy 等指标变化时,需要同时更新终端日志、可视化面板、企业微信告警等多个下游系统。传统做法是每个系统轮询指标存储,既滞后又浪费。
具体案例:某大模型训练平台实现一个 MetricSubject,指标每步更新后广播给日志观察者、曲线图画板观察者、邮件告警观察者;新增一个监控渠道只需注册新的观察者,训练代码零改动。
提示词安全过滤链(责任链模式)
阶段:AI 应用上线前的内容安全审核
解决的问题:用户输入到 LLM 之前要经过多道安全检查:长度、隐私、敏感词、注入攻击。硬编码一堆 if 判断既难维护又无法插拔。
具体案例:某 AI 客服系统使用责任链模式构建 Prompt 安全网关:长度限制节点 → 敏感词过滤节点 → 提示注入检测节点 → 人工审核节点,每个节点决定拦截还是放行,新安全策略只需追加一个节点。
Python 演示
from abc import ABC, abstractmethod class PayStrategy(ABC): @abstractmethod def pay(self, amount: float) -> float: pass class CreditCard(PayStrategy): def __init__(self, rate: float = 0.02): self.rate = rate def pay(self, amount: float) -> float: return amount * (1 + self.rate) class PointsPay(PayStrategy): def __init__(self, points_per_yuan: int = 10): self.ppk = points_per_yuan def pay(self, amount: float) -> float: return amount / self.ppk class Cash(PayStrategy): def pay(self, amount: float) -> float: return amount * 0.95 class PaymentContext: def __init__(self, strategy: PayStrategy): self.strategy = strategy def set_strategy(self, strategy: PayStrategy) -> None: self.strategy = strategy def checkout(self, amount: float) -> float: result = self.strategy.pay(amount) print(f"实付金额: {result:.2f}") return result if __name__ == "__main__": ctx = PaymentContext(CreditCard()) ctx.checkout(100) ctx.set_strategy(PointsPay(10)) ctx.checkout(100) ctx.set_strategy(Cash()) ctx.checkout(100)
checkout 主流程。class PayStrategy(ABC)定义策略接口,所有算法都必须实现pay方法,相当于餐厅的“支付方式”统一标准。class CreditCard(PayStrategy)一种具体策略,在支付金额上增加 2% 手续费。class PointsPay(PayStrategy)另一种策略,把金额折算成积分扣除。def checkout(self, amount)上下文只依赖策略接口,不关心具体算法,这是解耦的关键。def set_strategy(self, strategy)“动态切换”就是这个方法实现的,客人结账前随时改变支付方式。ctx.set_strategy(PointsPay(10))运行时给上下文换一个新策略,后续调用自动使用新行为。
常见误区
误区状态模式和策略模式的 UML 图很像,都是“上下文持有某个对象,运行时切换”,所以它们是一回事,可以随便互换。
正解两者关注点完全不同。策略模式是“外部调用者主动决定用哪个算法”,像顾客自己选辣度;状态模式是“对象内部状态转移驱动行为变化”,像订单到了“制作中”自动进入下一套规则。策略是选择,状态是演化。
为何常见:两者的类结构高度相似——都有 Context、抽象接口、多个实现类,初学者看到结构相同就忽略了意图差异。
误区观察者注册到主题后就不需要再管它,它会一直收到通知,反正主题会保存引用,很省心。
正解如果不及时注销不再使用的观察者,主题会一直持有它的引用,垃圾回收无法回收它,最终造成内存泄漏。就像餐厅广播在打烊后还在循环播放,但服务员早已下班离开,信息白白浪费。
为何常见:观察者模式的使用简单,注册即回调,很多人忽略了“反注册”同样是生命周期管理的一部分。
误区命令模式就是给按钮点击用的,把点击事件包成一个命令对象,其他场景用不上。
正解命令模式的核心价值是把“请求”对象化,因此可以用在任何需要解耦、排队、撤销、日志记录的场合:批任务调度、事务回滚、操作历史记录。小票不只给顾客看,还可以用于排单、退菜、对账,命令对象同理。
为何常见:教科书里最常见的就是 GUI 的撤销/重做例子,导致很多人以为命令模式只能用于界面层。
误区把请求丢进责任链就一定能得到处理,因为链上有这么多节点,总有一个会接住。
正解如果链尾也没有节点处理请求,请求会被静默丢弃。工程上必须设计兜底节点或默认处理者,就像投诉到经理那里已经是最后一环,如果经理不受理,投诉就石沉大海了。
为何常见:链式结构让人本能地以为“每条链都有终点”,但模式的默认行为并不保证处理——链路可能断裂,节点也可能不匹配。
面试题
Q1. 在模板方法模式中,允许子类在流程中插入额外逻辑而不改变骨架的方法通常被称为?
- A.钩子方法← 正确答案
- B.工厂方法
- C.抽象方法
- D.静态方法
答案:A。钩子方法在父类中提供默认实现(常常为空),子类可以覆盖它来插入逻辑,流程骨架不变,就像 SOP 允许师傅在装盘前撒一把自己的秘制葱花。
Q2. 观察者模式中,当主题状态变化时自动触发哪个核心操作?
- A.create()
- B.notify()← 正确答案
- C.destroy()
- D.render()
答案:B。主题状态变化后调用 notify() 遍历所有观察者并触发 update()。就像后厨广播“菜好了”,所有关注这个消息的人自动响应,而不是逐个单独询问。
Q3. 策略模式与状态模式的核心区别是什么?
- A.策略由外部调用者切换,状态由内部转移驱动← 正确答案
- B.两者完全相同
- C.状态模式只能用一次
- D.策略模式不支持运行时切换
答案:A。策略模式强调“可选算法由客户端选择”,状态模式强调“状态的内部演化决定行为”。顾客选辣度是策略;订单从待接单进入制作中是状态。两者结构相似、意图不同。
Q4. 在命令模式中,哪个角色负责真正执行业务逻辑?
- A.Invoker
- B.Command
- C.Receiver← 正确答案
- D.Client
答案:C。调用者触发命令,命令持有接收者引用并调用其方法,真正干活的是接收者 Receiver。就像小票由服务员写下、由后厨照做,厨师才是真正完成做菜的人。
Q5. 以下哪些模式可以降低对象之间的耦合度?
- A.观察者模式← 正确答案
- B.中介者模式← 正确答案
- C.责任链模式← 正确答案
- D.模板方法模式
答案:ABC。观察者让主题不依赖具体观察者,责任链让发送者不依赖具体处理者,中介者让同事对象只依赖一个中枢。这三者都切断了对象间的直接依赖,而模板方法主要是复用流程骨架。
Q6. 关于责任链模式,下列说法正确的有?
- A.可以动态调整链中的节点← 正确答案
- B.请求可能最终无人处理← 正确答案
- C.发送者必须知道链上全部节点
- D.每个节点都能修改请求内容← 正确答案
答案:ABD。责任链的节点可增删可排序;如果所有节点都不处理,请求会被丢弃;节点可以在传递前修改请求,但发送者完全不需要知道链上有谁。C描述的“必须知道全部节点”恰恰违背了解耦的初衷。
Q7. 三个责任链节点处理请求的成功率分别为 p₁=0.2、p₂=0.3、p₃=0.4,三者独立,请求在链上至少被一个节点处理的概率是多少?
- A.0.024
- B.0.664← 正确答案
- C.0.900
- D.0.500
答案:B。至少一个成功 = 1 - 全部失败 = 1 - (1-0.2)(1-0.3)(1-0.4) = 1 - 0.8×0.7×0.6 = 1 - 0.336 = 0.664。就像级级上报的投诉,只有每一级都不受理,投诉才真的没人接。
Q8. 要在命令模式中实现完整的“撤销”能力,关键做法是什么?
- A.每个命令对象保存对应的逆操作← 正确答案
- B.删除调用者中的命令对象
- C.使用全局静态变量记录状态
- D.让接收者继承命令接口
答案:A。撤销的核心是让命令对象持有 undo() 逆操作,执行时把反操作压入历史栈,撤销时弹出执行。就像小票注明“可退”,后厨按照注明执行逆向动作,仅仅删除对象或全局变量都只能丢失历史。
Q9. 在观察者模式中,推送(push)和拉取(pull)的区别是?
- A.推送由 Subject 主动发送数据给 Observer← 正确答案
- B.拉取由 Observer 在 update 时主动获取数据← 正确答案
- C.推送只能用于移动端
- D.拉取只能用于 Web 端
答案:AB。推送是主题在 notify 时把数据绑到 update 参数里,像广播把菜名都喊出来;拉取是主题只发一个信号,观察者自己来取数据,像广播喊“菜好了”,服务员自己去窗口端。C 和 D 完全是误导,与平台无关。
Q10. 在观察者模式的通知过程中,若 Observer 在 update() 里又去修改 Subject 的状态,最可能的风险是什么?
- A.触发递归通知导致栈溢出← 正确答案
- B.观察者永远收不到通知
- C.Subject 自动销毁
- D.所有观察者被强制注销
答案:A。update() 中修改 Subject,会再次触发 notify(),如果形成循环依赖(A 通知 B,B 修改 A 再通知 A),就会无限递归直到栈溢出。就像服务员听到广播后又去后厨加了一个新广播,广播套广播,整个餐厅被噪音淹没。