想象一家生意火爆的餐厅:后厨有川菜、粤菜、西餐、甜品四位师傅;服务员端着托盘穿梭;大堂经理在前厅八方调度。怎样才能让这支队伍高效配合,又不互相干扰?答案是——每个人只管好自己的事,同时遵循一套清晰的“协作规则”。顾客说想吃辣,厨师先问“微辣还是特辣”;后厨出餐时广播喊一句“十三号桌的菜好了”,所有需要知道的人自动响应;顾客要求退菜,服务员按流程执行,不必清楚后厨如何改菜。 软件世界里的对象,就像餐厅里的角色。行为型模式(Behavioral Patterns)就是一套编排对象间“协作规则”的智慧结晶:它关注职责分配、通信方式和控制流转移,让对象既能独立演化,又能默契配合。策略的切换、状态的流转、请求的层层传递,背后都有宝藏可挖。本文就用这家餐厅的日常运转,带你读懂行为型模式的八大核心招法。

岗位关联

一句话职业价值:行为型模式是搭建可扩展、可维护 AI 系统的底层思维——训练流程编排、多模型调度、Agent 协作都离不开这些协作套路。

目录
  1. 可视化演示
  2. 策略模式:菜单上可换的招牌菜
  3. 观察者模式:广播声响起,各岗位自动响应
  4. 命令模式:一张小票把请求变成对象
  5. 迭代器模式:服务员按顺序上菜
  6. 状态模式:订单跳动的生命周期
  7. 模板方法模式:标准作业流程
  8. 责任链模式:顾客投诉的逐级上报
  9. 中介者模式:大堂经理的调度中枢
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

策略模式:菜单上可换的招牌菜

顾客在餐厅点“宫保鸡丁”,菜单上写着“辣度可选:微辣/中辣/特辣”。后厨不用更换整套烹饪流程——锅还是那口锅,料还是那些料,只是换掉“辣度方案”。这就是策略模式最朴素的场景:同一个任务,由调用者选择一种算法来执行。

软件世界里,“同一道菜”往往对应“同一个业务接口”:排序可以用快排也可以用归并;AI 推理可以用 GPU 满精度,也可以切到 CPU 量化。策略模式(Strategy Pattern)把一族算法各自封装成独立的“策略类”,让上下文(Context)通过统一接口调用。所谓“耦合”,就是两个模块纠缠在一起难以分离;策略模式的目的是把“用哪个算法”和“怎么用”彻底拆开。

策略模式包含三个角色:上下文持有策略引用,负责调用;策略接口定义统一方法;具体策略类实现各自的算法细节。调用方在运行时可以动态替换策略——就像顾客临时改口“换特辣”,后厨只需换一个调料方案。用公式表达:

策略模式的可替换性,正如后厨换辣度方案而不动灶台——上下文在运行时调用同一接口,却能在不同算法间平滑切换。理解这点之后,你会发现 AI 里的“推理后端选择”本质上也是一道策略选择题。

s∗=arg⁡min⁡s∈SCost(s,x)s^* = \displaystyle\arg\min_{s \in S} \text{Cost}(s, x)
这个公式的意思是:面对输入 $x$,我们从策略集合 $S$ 中挑选一个让代价函数最小的策略 $s^*$。就像选辣度时,顾客根据“今天想吃多刺激”来决定选哪一种。
  1. 策略集合 S所有可替换算法构成的集合,对应后厨的“辣度方案表”:
    S={s1,s2,…,sn}S = \{s_1, s_2, \dots, s_n\}
  2. 代价函数 Cost衡量某个策略在当前输入 $x$ 下表现好坏的标准,相当于顾客对“辣度指数”的权衡:
    Cost(s,x)↦R\text{Cost}(s, x) \mapsto \mathbb{R}
  3. argmin 运算从集合中挑出代价最小的那一项,而不是返回代价本身:
    arg⁡min⁡s∈S(⋅)\arg\min_{s \in S}(\cdot)
  4. 最优策略 s\*被选中的策略对象,最终被上下文调用执行:
    s∗∈Ss^* \in S

观察者模式:广播声响起,各岗位自动响应

餐厅后厨有一个传菜口,菜好了就对着广播喊:“十三号桌的菜好了!”服务员听到了去上菜,外卖员听到了去打包,领位员听到了知道这桌快翻台了。没有人去逐一询问“你要不要这道菜的消息”——信息自己扩散到所有关心的人那里。

观察者模式正是这样一种“一对多”的通知机制。主题(Subject)维护一份观察者列表,状态变化时调用 notify() 广播事件;观察者(Observer)注册后,就会在主题变化时自动收到更新。模型训练时,指标变化要同时通知日志、可视化面板和告警机器人——这就是观察者模式的典型战场。

主题不需要知道观察者是谁,观察者也不需要主动轮询,双方只依赖抽象的订阅关系。这种松耦合让系统像广播一样自然:新加一个观察者,只需注册;移除一个,只需注销。通知过程可以形式化为:

后厨的广播不需要知道谁在听,只要有心人,就会听到。观察者模式让对象之间形成“发消息不用点名”的自然协作关系,这正是事件驱动架构的基石。

notify(e)=⋃o∈Oupdate(o,e)\text{notify}(e) = \displaystyle\bigcup_{o \in O} \text{update}(o, e)
右边是说:对集合 $O$ 中每一个观察者 $o$,都执行一次 update;所有更新动作并在一起,就完成了事件 $e$ 的广播。
  1. 观察者集合 O所有注册了“收听”的对象,相当于听到广播后行动起来的人员:
    O={o1,o2,…,ok}O = \{o_1, o_2, \dots, o_k\}
  2. update 方法每个观察者收到事件后的响应逻辑,类似服务员听到菜好后去端菜:
    update(oi,e)\text{update}(o_i, e)
  3. 并集运算对所有观察者逐个触发更新,不遗漏任何一个注册者:
    ⋃o∈O\bigcup_{o \in O}
  4. 广播结果一次 notify 调用就能把事件送达所有关心它的对象:
    notify(e)\text{notify}(e)

命令模式:一张小票把请求变成对象

顾客向服务员说“来一份宫保鸡丁,微辣”,服务员不会把这句话原样口传给厨师,而是写下一张小票:菜名、辣度、桌号。后厨照着执行,叫号时凭票取餐。这张小票,就是把“请求”变成实在物体的过程。

命令模式(Command Pattern)把一次方法调用封装成独立的命令对象。调用者(Invoker)只负责在合适时机触发 execute();命令对象内部保存接收者(Receiver)和必要参数;接收者真正动手干活。小票能排队、能改单、能退菜,命令对象同样支持延迟执行、日志记录、撤销重做。

当你点“撤销”时,看似是系统的神奇能力,其实是命令对象保存了逆操作 undo()——就像小票注明“可退”,后厨照做。命令模式让请求具备“物质形态”,因此可以在系统里排队、存档、回放:

小票把顾客的嘴巴和厨师的手解耦——顾客不必进厨房,厨师不必听口述。命令模式让请求脱离发送者独立存在,从而解锁排队、撤销、日志等一系列能力,这是 AI 任务调度系统中极其常见的套路。

R=Command.execute(params)R = \text{Command.execute}(\text{params})
命令对象把参数、接收者和动作打包进 execute(),调用者触发它后得到结果 $R$。小票上写的东西越清楚,后厨执行越可靠;命令对象封装得越好,系统越灵活。
  1. 调用者 Invoker持有命令对象的引用,并在合适的时机触发命令,像服务员接过小票后交给后厨:
    Invoker→Command\text{Invoker} \to \text{Command}
  2. 命令对象 Command包含接收者引用和执行所需参数,核心方法就是执行动作:
    Command.execute(params)\text{Command.execute}(\text{params})
  3. 接收者 Receiver真正执行业务逻辑的对象,像后厨的师傅照着小票做菜:
    Receiver←Command\text{Receiver} \leftarrow \text{Command}
  4. 执行结果 R命令执行完毕返回的结果,可能是布尔值、数据或状态变更:
    R∈ResultR \in \text{Result}

迭代器模式:服务员按顺序上菜

服务员给一桌上菜,不需要知道后厨有几口锅、冰箱在哪个角落,她只需要知道:这一桌还剩几道菜,下一道该端什么。顺序,就是她对“上菜”这个集合的唯一认知。

迭代器模式(Iterator Pattern)让你以同样方式遍历一个集合:不需要知道底层是数组、链表还是树,只要拿到一个游标,依次问道“还有吗?给我下一个”。hasNext() 判断是否还有元素,next() 返回当前元素并移动游标。

集合暴露的越少,内部越容易变化。今天用数组存储,明天改成链表,只要迭代器接口不变,调用方代码一行都不用改。这就像服务员只认“几号桌、第几道菜”,不管后厨用什么灶:

服务员无须进入厨房,迭代器无须暴露内部结构——遍历逻辑和存储结构彻底分离,这在处理大规模数据集(如训练批次取样)时至关重要。

hasNext()∧next()⇒{e1,e2,…,en}\text{hasNext}() \land \text{next}() \Rightarrow \{e_1, e_2, \dots, e_n\}
“还有菜吗”和“上下一道”这两个操作配合起来,就能从头到尾遍历整个序列。服务员不拆开厨房,调用者也不拆开集合,这正是迭代器的优雅之处。
  1. hasNext 判断检查集合中是否还有未遍历的元素,像服务员看了一眼托盘还有几道菜:
    hasNext()↦{true,false}\text{hasNext}() \mapsto \{true, false\}
  2. next 取元素返回游标当前指向的元素,并把游标后移一位:
    next()↦ei\text{next}() \mapsto e_i
  3. 元素序列被遍历的底层数据容器,对迭代器而言只是一个黑盒:
    {e1,e2,…,en}\{e_1, e_2, \dots, e_n\}
  4. 循环条件hasNext() 为真时才继续取下一个,保证不会越界:
    hasNext()∧next()\text{hasNext}() \land \text{next}()

状态模式:订单跳动的生命周期

一张订单在餐厅里的命运清晰而严格:待接单 → 制作中 → 已出餐。待接单时,顾客可以取消;制作中就不能随便取消;已出餐后,连改口味都不行。每个状态有自己允许的操作,流程本身像一个会自动跳转的装置。

如果在代码里用一堆 if 判断“当前是哪个状态”,状态一多,代码里到处是条件分支,改一个状态就要动所有地方。状态模式(State Pattern)把每个状态封装成独立的状态类,上下文对象保存一个“当前状态对象”,事件到来时委托给它处理;状态类内部还可以决定流转到哪个下一状态。

订单状态的流转,本质上是一个状态转移函数:给定当前状态和触发事件,推出下一个状态。这像极了餐厅运转规则——状态对象就是“规则卡片”:

订单就像一个有生命的个体,每个阶段都由“规则”自动接管。状态模式把杂乱的条件判断重构成清晰的状态类集合,在 AI Agent 的多阶段任务流程中也大有用武之地。

St+1=δ(St,Et)S_{t+1} = \delta(S_t, E_t)
当前状态是 $S_t$,来了一个事件 $E_t$(比如“厨师开始炒菜”),通过转移函数 $\delta$ 决定下一个状态 $S_{t+1}$。一张订单就这样被规则卡片推着往前走。
  1. 状态集合 S对象可能处于的全部状态,对应订单的三个阶段:
    S={S1,S2,…,Sn}S = \{S_1, S_2, \dots, S_n\}
  2. 触发事件 E能让状态发生变化的输入信号,像“下单”“开始制作”“出餐”:
    Et∈EE_t \in \mathcal{E}
  3. 转移函数 δ输入当前状态和事件,输出下一状态,是状态机的核心规则:
    δ(St,Et)↦St+1\delta(S_t, E_t) \mapsto S_{t+1}
  4. 状态更新上下文把内部状态替换为转移结果,行为随之改变:
    Context.state←St+1\text{Context.state} \leftarrow S_{t+1}

模板方法模式:标准作业流程

餐厅后厨有一份《标准作业流程》:备料 → 开火 → 下锅 → 翻炒 → 装盘。不管是川菜师傅还是粤菜师傅,都必须按这个骨架走;不同的是,下锅后的具体手法各有高招。菜单换了,骨架却不换——这就是流程复用的力量。

模板方法模式(Template Method Pattern)在父类中定义一个算法的骨架,把某些具体步骤延迟给子类实现。父类的模板方法负责按固定顺序调用步骤;子类只需实现自己关心的“变化点”,保证整体流程一致。

除了抽象步骤,模板方法还提供钩子方法(hook method):父类里有一个默认实现或空实现,子类可以选择覆盖它,插入额外逻辑而不破坏流程。这就像 SOP 允许师傅在装盘前撒一把自己的秘制葱花:

SOP 没有教厨师如何为每道菜调味,却保证了出餐流程一致——模板方法模式同样如此。在 AI 领域,标准的训练流水线、推理流水线都可以用模板方法固化骨架,把“换模型头”“换损失函数”留作子类钩子。

template()=step1()→step2()→⋯→stepn()\text{template}() = \text{step}_1() \to \text{step}_2() \to \dots \to \text{step}_n()
模板方法是多个步骤按固定顺序组合出来的:骨架固定,细节分给子类。这保证每道菜流程统一,却各有风味。
  1. 流程步骤链算法骨架由一串按顺序执行的步骤组成:
    step1()→step2()→⋯→stepn()\text{step}_1() \to \text{step}_2() \to \cdots \to \text{step}_n()
  2. 抽象步骤部分步骤在父类中只声明不实现,等待子类填充:
    stepi()∈{abstract,concrete}\text{step}_i() \in \{\text{abstract}, \text{concrete}\}
  3. 钩子方法子类可选的覆盖点,通常有默认空实现:
    hook()↦void\text{hook}() \mapsto \text{void}
  4. 模板执行骨架 + 子类细节组合成完整算法,保证可复用与可扩展:
    template()↦Rfinal\text{template}() \mapsto R_{\text{final}}

责任链模式:顾客投诉的逐级上报

顾客不满意了,先向服务员提。服务员解决不了,上报领班;领班拿不准,请经理出马。这条“投诉链”上,每个节点都有机会处理,处理不了就往上传。顾客不需要知道最终是谁解决的,他只需要把不满说出来。

责任链模式(Chain of Responsibility Pattern)把多个处理者串成一条链,请求沿着链依次传递,每个节点自主决定“我处理”还是“传给下一个”。发送者与接收者彻底解耦——发送者不知道也不关心链上有谁。

这给系统带来极大的动态性:可以随时增删处理节点、调整顺序。比如 AI 内容审核流程:长度检查 → 敏感词过滤 → 语义审核 → 人工复审,任何一个环节拦截住,请求就终止。假设每个节点独立处理成功的概率为 $p_i$,则整条链至少有一个节点处理成功的概率是:

每一环只问一句“这事我管不管?”,不管就往下传——责任链让请求在链上流动,让责任在各环间分散,也让 AI 流水线中的前置检查逻辑变得轻盈可插拔。

P(handled)=1−∏i=1n(1−pi)P(\text{handled}) = 1 - \displaystyle\prod_{i=1}^{n}\big(1 - p_i\big)
每个节点都失败,请求才会漏过去。所以“至少一个节点成功”等于 1 减去“全部失败”的概率,就像一个投诉只要有一级接住,就算被处理。
  1. 单节点成功率第 $i$ 个节点独立处理请求的概率,代表这一级“接得住”的能力:
    pip_i
  2. 全部失败概率每个节点都处理不了,请求才无人接住:
    ∏i=1n(1−pi)\prod_{i=1}^{n}(1 - p_i)
  3. 补集运算用 1 减去“全部失败”,得到至少一个节点成功的概率:
    1−∏i=1n(1−pi)1 - \prod_{i=1}^{n}(1 - p_i)
  4. 工程含义链越长,请求被处理的概率越高,但每个节点都要消耗时间,所以要在覆盖率与延迟之间权衡:
    P(handled)→1P(\text{handled}) \to 1

中介者模式:大堂经理的调度中枢

如果餐厅没有大堂经理,服务员要自己去找后厨、收银、仓管、外卖骑手逐一沟通:菜要催、单要改、餐具要补……关系乱成一团麻。有了大堂经理,一切协调都找他:他统一调配、传话、决策,大家只需要和他沟通。

中介者模式(Mediator Pattern)用一个中介对象封装一组对象的交互,把网状结构变成星型结构。每个同事对象只持有中介者的引用,消息发给中介者,由它转给目标对象。这样对象之间互不知道彼此的存在。

交互逻辑被集中到中介者身上,修改协作规则时只改中介者,不用逐个改动同事类。代价是中介者本身可能变得复杂,所以要把控交互规则的边界。通信路径可以简洁表示为:

星型的沟通结构,正是中介者模式的核心:协作规则的复杂度收拢到一处,其他人只需守好自己的岗位。在多智能体协作系统中,中介者模式经常扮演“总控 Agent”的角色。

Oi→M→Oj,∀i≠jO_i \to M \to O_j, \quad \forall i \neq j
原本 $O_i$ 要直接和 $O_j$ 通信,现在统一经过中介者 $M$。后厨不直接和每一桌顾客喊话,大堂经理一声令下,全员各就各位。
  1. 同事对象集合原本需要互相通信的一组协作对象:
    {O1,O2,…,On}\{O_1, O_2, \dots, O_n\}
  2. 中介者 M集中所有交互逻辑的核心对象,扮演调度者角色:
    M←规则集合M \leftarrow \text{规则集合}
  3. 通信路径任何两个同事之间的消息都换成间接传递:
    Oi→M→OjO_i \to M \to O_j
  4. 链路数量对比直接通信需 $n \times (n-1)$ 条链路,星型结构只需 $n$ 条:
    边数:n(n−1)2⇒n\text{边数}: \frac{n(n-1)}{2} \Rightarrow n

AI 应用场景

动态推理策略切换(策略模式)

阶段:模型部署与推理优化

解决的问题:传统的单一推理引擎难以兼顾不同输入——短文本用 GPU 跑浪费资源,长文本 CPU 又太慢。用策略模式将“高精度 GPU 推理”“CPU 快速推理”“量化加速推理”封装为策略类,推理服务器根据请求长度、延迟预算动态切换。

具体案例:某 OCR 在线服务在晚高峰流量激增时,自动将低置信度优先策略切换为“批量优先”策略;同一份图片,不同时段走不同推理策略,成本降低约 35%。

训练指标广播监控(观察者模式)

阶段:模型训练与监控

解决的问题:训练过程中 loss、accuracy 等指标变化时,需要同时更新终端日志、可视化面板、企业微信告警等多个下游系统。传统做法是每个系统轮询指标存储,既滞后又浪费。

具体案例:某大模型训练平台实现一个 MetricSubject,指标每步更新后广播给日志观察者、曲线图画板观察者、邮件告警观察者;新增一个监控渠道只需注册新的观察者,训练代码零改动。

提示词安全过滤链(责任链模式)

阶段:AI 应用上线前的内容安全审核

解决的问题:用户输入到 LLM 之前要经过多道安全检查:长度、隐私、敏感词、注入攻击。硬编码一堆 if 判断既难维护又无法插拔。

具体案例:某 AI 客服系统使用责任链模式构建 Prompt 安全网关:长度限制节点 → 敏感词过滤节点 → 提示注入检测节点 → 人工审核节点,每个节点决定拦截还是放行,新安全策略只需追加一个节点。

Python 演示

python50 行
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 主流程。
  1. class PayStrategy(ABC)定义策略接口,所有算法都必须实现 pay 方法,相当于餐厅的“支付方式”统一标准。
  2. class CreditCard(PayStrategy)一种具体策略,在支付金额上增加 2% 手续费。
  3. class PointsPay(PayStrategy)另一种策略,把金额折算成积分扣除。
  4. def checkout(self, amount)上下文只依赖策略接口,不关心具体算法,这是解耦的关键。
  5. def set_strategy(self, strategy)“动态切换”就是这个方法实现的,客人结账前随时改变支付方式。
  6. ctx.set_strategy(PointsPay(10))运行时给上下文换一个新策略,后续调用自动使用新行为。

常见误区

误区状态模式和策略模式的 UML 图很像,都是“上下文持有某个对象,运行时切换”,所以它们是一回事,可以随便互换。

正解两者关注点完全不同。策略模式是“外部调用者主动决定用哪个算法”,像顾客自己选辣度;状态模式是“对象内部状态转移驱动行为变化”,像订单到了“制作中”自动进入下一套规则。策略是选择,状态是演化。

为何常见:两者的类结构高度相似——都有 Context、抽象接口、多个实现类,初学者看到结构相同就忽略了意图差异。

误区观察者注册到主题后就不需要再管它,它会一直收到通知,反正主题会保存引用,很省心。

正解如果不及时注销不再使用的观察者,主题会一直持有它的引用,垃圾回收无法回收它,最终造成内存泄漏。就像餐厅广播在打烊后还在循环播放,但服务员早已下班离开,信息白白浪费。

为何常见:观察者模式的使用简单,注册即回调,很多人忽略了“反注册”同样是生命周期管理的一部分。

误区命令模式就是给按钮点击用的,把点击事件包成一个命令对象,其他场景用不上。

正解命令模式的核心价值是把“请求”对象化,因此可以用在任何需要解耦、排队、撤销、日志记录的场合:批任务调度、事务回滚、操作历史记录。小票不只给顾客看,还可以用于排单、退菜、对账,命令对象同理。

为何常见:教科书里最常见的就是 GUI 的撤销/重做例子,导致很多人以为命令模式只能用于界面层。

误区把请求丢进责任链就一定能得到处理,因为链上有这么多节点,总有一个会接住。

正解如果链尾也没有节点处理请求,请求会被静默丢弃。工程上必须设计兜底节点或默认处理者,就像投诉到经理那里已经是最后一环,如果经理不受理,投诉就石沉大海了。

为何常见:链式结构让人本能地以为“每条链都有终点”,但模式的默认行为并不保证处理——链路可能断裂,节点也可能不匹配。

面试题

Q1. 在模板方法模式中,允许子类在流程中插入额外逻辑而不改变骨架的方法通常被称为?

单选题 · easy

  • A.钩子方法← 正确答案
  • B.工厂方法
  • C.抽象方法
  • D.静态方法

答案:A。钩子方法在父类中提供默认实现(常常为空),子类可以覆盖它来插入逻辑,流程骨架不变,就像 SOP 允许师傅在装盘前撒一把自己的秘制葱花。

Q2. 观察者模式中,当主题状态变化时自动触发哪个核心操作?

单选题 · easy

  • A.create()
  • B.notify()← 正确答案
  • C.destroy()
  • D.render()

答案:B。主题状态变化后调用 notify() 遍历所有观察者并触发 update()。就像后厨广播“菜好了”,所有关注这个消息的人自动响应,而不是逐个单独询问。

Q3. 策略模式与状态模式的核心区别是什么?

单选题 · mid

  • A.策略由外部调用者切换,状态由内部转移驱动← 正确答案
  • B.两者完全相同
  • C.状态模式只能用一次
  • D.策略模式不支持运行时切换

答案:A。策略模式强调“可选算法由客户端选择”,状态模式强调“状态的内部演化决定行为”。顾客选辣度是策略;订单从待接单进入制作中是状态。两者结构相似、意图不同。

Q4. 在命令模式中,哪个角色负责真正执行业务逻辑?

单选题 · mid

  • A.Invoker
  • B.Command
  • C.Receiver← 正确答案
  • D.Client

答案:C。调用者触发命令,命令持有接收者引用并调用其方法,真正干活的是接收者 Receiver。就像小票由服务员写下、由后厨照做,厨师才是真正完成做菜的人。

Q5. 以下哪些模式可以降低对象之间的耦合度?

多选题 · mid

  • A.观察者模式← 正确答案
  • B.中介者模式← 正确答案
  • C.责任链模式← 正确答案
  • D.模板方法模式

答案:ABC。观察者让主题不依赖具体观察者,责任链让发送者不依赖具体处理者,中介者让同事对象只依赖一个中枢。这三者都切断了对象间的直接依赖,而模板方法主要是复用流程骨架。

Q6. 关于责任链模式,下列说法正确的有?

多选题 · mid

  • A.可以动态调整链中的节点← 正确答案
  • B.请求可能最终无人处理← 正确答案
  • C.发送者必须知道链上全部节点
  • D.每个节点都能修改请求内容← 正确答案

答案:ABD。责任链的节点可增删可排序;如果所有节点都不处理,请求会被丢弃;节点可以在传递前修改请求,但发送者完全不需要知道链上有谁。C描述的“必须知道全部节点”恰恰违背了解耦的初衷。

Q7. 三个责任链节点处理请求的成功率分别为 p₁=0.2、p₂=0.3、p₃=0.4,三者独立,请求在链上至少被一个节点处理的概率是多少?

单选题 · hard

  • 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. 要在命令模式中实现完整的“撤销”能力,关键做法是什么?

单选题 · hard

  • A.每个命令对象保存对应的逆操作← 正确答案
  • B.删除调用者中的命令对象
  • C.使用全局静态变量记录状态
  • D.让接收者继承命令接口

答案:A。撤销的核心是让命令对象持有 undo() 逆操作,执行时把反操作压入历史栈,撤销时弹出执行。就像小票注明“可退”,后厨按照注明执行逆向动作,仅仅删除对象或全局变量都只能丢失历史。

Q9. 在观察者模式中,推送(push)和拉取(pull)的区别是?

多选题 · mid

  • A.推送由 Subject 主动发送数据给 Observer← 正确答案
  • B.拉取由 Observer 在 update 时主动获取数据← 正确答案
  • C.推送只能用于移动端
  • D.拉取只能用于 Web 端

答案:AB。推送是主题在 notify 时把数据绑到 update 参数里,像广播把菜名都喊出来;拉取是主题只发一个信号,观察者自己来取数据,像广播喊“菜好了”,服务员自己去窗口端。C 和 D 完全是误导,与平台无关。

Q10. 在观察者模式的通知过程中,若 Observer 在 update() 里又去修改 Subject 的状态,最可能的风险是什么?

单选题 · extreme

  • A.触发递归通知导致栈溢出← 正确答案
  • B.观察者永远收不到通知
  • C.Subject 自动销毁
  • D.所有观察者被强制注销

答案:A。update() 中修改 Subject,会再次触发 notify(),如果形成循环依赖(A 通知 B,B 修改 A 再通知 A),就会无限递归直到栈溢出。就像服务员听到广播后又去后厨加了一个新广播,广播套广播,整个餐厅被噪音淹没。