引言
设计模式(Design Pattern)是软件开发中经过验证的最佳实践,用于解决在软件设计中常见的问题。
经典设计模式可分为三种:
| 类型 | 关注点 |
|---|---|
| 创建型 | 对象怎么来 |
| 结构型 | 对象怎么组装 |
| 行为型(本章节) | 对象间怎么协作 |
行为型最多,先从这里讲起
责任链
将对象连成一条链,沿着链传递请求,直到被一个对象处理为止
举个栗子:你要请假,但不同的请假时长需要由不同级别的领导审批
请假 1~3 天 → 直属组长审批
请假 4~7 天 → 部门经理审批
请假 8~15 天 → 总监审批
请假 15 天以上 → CEO 审批
先来看图

假条会沿着责任链依次往下,组长批不了就部门经理批,还批不了就总监批
组长 -> 部门经理 -> 总监 -> CEO
在责任链中,发送者和请求者解耦,发送者无需考虑是谁处理了请求,只需将请求丢到链中
每个处理者职责单一,若自己处理不了,就向下游丢
责任链符合开闭原则,即使需要在链中增减处理者,对已有代码影响很小
命令
将请求封装为一个对象
今天你突然想吃汉堡,来到了蟹老板的餐厅
对于顾客,你只是找到章鱼哥(Invoker)点了一份蟹堡,等待一会蟹堡就做好了

在此过程中,顾客(Client) new了一份 订单(Command)的对象
章鱼哥(Invoker)仅持有订单对象,执行对象的execute(),将其交由后厨(Recever)
后厨海绵宝宝(Recever)根据订单内容执行,订单上是什么就做什么
可以看出,在命令模式中:
-
调用者和接收者彻底解耦,Invoker只考虑接收Command,并不负责执行
-
Recever根据接收到的不同的Command,执行不同的操作
-
Command就像变量一样,被传递和存储,Invoker可以将Command放入队列,也可以记录成日志
-
每个新命令,只需创建一个类,不需要修改Invoker和Recever的代码
解释器
提供语言的语法或表达式,使用解释器表达解释该语言的句子
喝奶茶的时候,不知道各位有没有注意过标签

我们简化下 珍3/糖0.5/奶250/热水至500
聪明的你应该大致能看出来,三分珍珠、半糖、奶25ml、剩下的热水加满到500ml
你理解这段语言的过程,就是一个解释器,来看图 :

迭代器
允许顺序访问一个聚合对象的元素,同时不暴露其内部表示
现在应该很少人看电视了,你有没有发现,观众只需要提供遥控器的上下键,就能顺序切换节目,而观众无需关注电视中到底是怎么存储的
而你手中的遥控器,就可以简单理解成是一个迭代器,来看图

在程序中,通常还会实现这两个方法:
-
isDone():用来表示是否遍历到头
-
currentItem():用来拿到当前元素
来段伪代码举例:
┌─────────────────────────────┐
│ while not isDone(): │ ← 1. 还没到头吗?
│ item = currentItem() │ ← 2. 拿到当前元素
│ 处理(item) │ ← 3. 对元素做操作
│ next() │ ← 4. 指针往后挪一步
└─────────────────────────────┘
中介者(Mediator)
用一个对象封装一系列对象交互,使对象间不需要显式的相互引用
想象一下,要是没人发明工作群这个东西,平时要怎么和同事之间互相沟通
团队五个人(A-E),两两私聊,就有10条沟通线
A↔B、A↔C、A↔D、A↔E、B↔C、B↔D、B↔E、C↔D、C↔E、D↔E
厚礼蟹,这也太麻烦了。好,现在聪明的你拉了个微信群,当起了群主,将其他4名同时都拉了进来
大家再也不用互相传递消息,有事在群里一发就好
这群,就是中介者(Mediator),看图:

各个对象不再互相引用,而是把所有交互逻辑集中到一个中介者对象里,由它来协调、转发、仲裁。
对于对象间存在复杂的网状引用有奇效
备忘录
在不破坏封装性的情况下,捕获并在外部保存对象的状态
大家打游戏的时候,有没有被同一个BOSS虐到崩溃的经历

通常游戏会在进入BOSS战前,游戏会有一个自动存档,每当被BOOS击败的时候,会自动读回之前的状态
这个存档,就是备忘录,来看图:

- 角色 = 发起人(Originator):拥有状态(血量、等级、装备……)
- 存档 = 备忘录(Memento):保存了某一时刻的完整状态快照
- 游戏系统的”存档/读档”菜单 = 管理者(Caretaker):负责保管存档文件,但不能打开修改它
观察者
当一个对象的状态发生改变,所有依赖它的对象都自动更新
这个好理解,各位应该都在B站或是别的平台,收到关注的博主的更新消息吧
先来看图:

博主(Subject)定义三个方法,attach、detach分别用于关注、取关;
notify方法用于在更新作品后,向所有的粉丝推送通知
class BiliUPHost(UPHost):
# ........
def notify(self, video_title: str):
"""发新视频,逐个推送给所有粉丝"""
self._latest_video = video_title
for fan in self._fans:
fan.update(video_title, self.name) # 调用粉丝的方法,发生通知
观察者模式最大的特点就是,使用发布-订阅,将两者送耦合
现实中通常使用MQ实现这种发布-订阅的模式
状态
同一个对象,状态不同,行为就不同
同一个”你”,在不同时间段(状态)下,面对同一个问题——“老板叫你”,反应完全不同:
| 时间段(状态) | 老板叫你,你的反应(行为) |
|---|---|
| 刚睡醒 | ”嗯?谁?……哦,闹钟。 |
| 挤地铁 | 假装没听见,戴上耳机 |
| 在上班 | ”好的老板,马上处理! |
| 干饭中 | 心里骂骂咧咧,含糊地说”稍等稍等” |
| 下班后 | ”您拨打的用户已关机。 |
来看图:

状态模式将状态以及执行的步骤进行了解耦,每单增删一个状态以及其行为时,只需要在实现、删除一个状态的实现即可,比直接写一堆IF语句优雅多了
# 不用状态模式的话
class Worker:
def boss_call(self):
if self.state == "睡觉":
print("嗯?谁?")
elif self.state == "挤地铁":
print("假装没听见")
elif self.state == "上班":
print("好的老板!")
elif self.state == "干饭":
print("稍等稍等")
elif self.state == "下班":
print("您拨打的用户已关机")
# ... 每加一个状态,这里就要加一个 elif
策略
将一系列算法封装成一个个类,并在需要时可相互替换
其实策略和状态两个模式在结构上几乎一样
我换个例子,通常我们在导航软件上,会有多条不同策略的路线选择(最短/最快/不走高速)
我们来看图,就能发起和状态模式及其相似:

似曾相识,对喽,我画图都直接只要CV改改
这两模式主要体现在意图不同,结构和作用上几乎是相同的
| 状态模式 | 策略模式 | |
|---|---|---|
| 核心意图 | 状态变了,行为自动跟着变 | 算法可以互相替换 |
| 状态之间 | 知道彼此,能触发切换(下班→睡觉) | 互不知道,彼此独立 |
| 谁切换 | 通常由状态自己决定下一个状态 | 由客户端主动选择用哪个策略 |
模板方法
搭一个骨架,子类需重写方法实现
无论是炒青菜还是炒牛肉,整体的流程是相同的,简化完无非就四步:
- 洗食材
- 切食材
- 下锅炒
- 装盘上桌
但是不同的食材,每一步的具体做法可能有差异
| 步骤 | 炒青菜 | 炒牛肉 |
|---|---|---|
| 洗 | 洗菜叶 | 洗牛肉 |
| 切 | 切段 | 切薄片、腌制 |
| 炒 | 大火快炒2分钟 | 猛火爆炒30秒 |
| 装盘 | 直接装盘 | 摆盘撒葱花 |
这时候,就可以先定义个一接口,然后在分别实现不同的做法
其实模板方法这一模式很简单且常用,看图:

访问者
元素的执行算法可随着访问者的改变而改变
东西(数据结构)是固定的,但要在它身上做的事(操作)经常变。于是把”做什么”抽出来,做成一个个独立的”访问者”
就像快递分拣中心,一天内会收到很多包裹,不同类型的包裹需要不同的处理方式
| 包裹类型 | 特点 |
|---|---|
| 普通件 | 随便扔,耐摔 |
| 生鲜件 | 必须走冷链,超时作废 |
| 易碎件 | 轻拿轻放,贴”向上”标签 |
而且同一个包裹,会经过不同人的手
- 分拣员:决定走哪条传送带
- 计价员:算运费(生鲜加钱、易碎加保价费)
- 打包员:决定用什么包装(泡沫箱 / 纸箱 / 保温箱)
- 质检员:检查是否破损、是否超时
即使包裹种类相对固定,但操作员可能会有变动增加
这时候就不适合将逻辑都塞到包裹类中,不然每加一个操作就要改所有包裹类,越改越臃肿。
访问者模式的做法是-包裹不动,让不同的人上门来”访问”它。看图:

该模式在新增操作极其容易:加一个 Inspector(质检员)访问者,一个包裹类都不用改
但反过来,新增元素类型(比如加个”危险品包裹”)就很痛苦:要改 Visitor 接口和所有已有访问者
参考资料:
《软件设计师教程》 - 清华大学出版社
《您的设计模式》 - 设计模式公司
一些AI给出的建议