Python 设计模式——备忘录模式(Memento Pattern)详解
本文面向零基础新手,从概念到“状态快照与恢复”,从简单示例到编辑器撤销,全方位讲解备忘录模式。
一、什么是备忘录模式?
1.1 通俗理解
备忘录模式是一种行为型设计模式,核心思想是:
在不破坏封装的前提下,把对象的“当前状态”保存成一份“备忘录”(快照);以后需要时,可以用这份备忘录把对象恢复到当时的状态。负责保存和恢复的“管理者”只持有备忘录对象,不直接接触对象内部细节,这样既支持撤销/存档,又不会让外部随意修改对象内部。
可以这样类比:
- 游戏存档:当前角色血量、位置、背包是一个复杂状态;存档 = 把这份状态“拍一张快照”存起来;读档 = 用快照把角色恢复成当时的样子。快照(备忘录)可以交给“存档管理器”保存到文件,管理器不需要知道状态里每个字段是什么意思,只要保管好快照即可。
- 编辑器撤销:每输入一段文字,编辑器就把“当前全文”保存成一个备忘录放进历史;点撤销时,从历史里取出上一个备忘录,把编辑器恢复成那个状态。备忘录只负责“存状态”,恢复时由编辑器自己根据备忘录还原。
所以,备忘录模式解决的是:需要“保存状态、稍后恢复”(撤销、存档、快照)时,把状态封装成备忘录,由管理者保管,由原对象自己负责“生成备忘录”和“从备忘录恢复”,从而不暴露内部结构。
1.2 为什么需要备忘录模式?
场景一:撤销(Undo)
编辑器、表单、配置界面等,用户操作后要能撤销。若把状态直接暴露给“历史记录”,历史记录就要依赖对象内部结构,耦合高且易出错。用备忘录:对象在关键点生成“状态快照”(备忘录),历史只存备忘录;撤销时把备忘录交回对象,由对象自己恢复,外部不碰内部字段。
场景二:存档 / 读档
游戏、工作流等要保存进度。把“当前状态”序列化成备忘录(或再存到文件),需要时反序列化后交给对象恢复。管理者(存档管理器)只负责存/取备忘录,不解析状态内容。
场景三:快照 / 回滚
配置、数据库等要做“某一时刻的快照”,之后可回滚。快照内容就是备忘录;回滚时用备忘录恢复,不破坏对象封装。
1.3 适用场景(什么时候用?)
| 场景 | 说明 |
|---|---|
| 撤销 / 重做 | 保存历史状态,支持回退或重做。 |
| 存档 / 读档 | 保存进度,之后恢复。 |
| 快照 / 回滚 | 某一时刻的状态备份与恢复。 |
简单记忆:需要把对象状态保存下来、以后能恢复,又不想让外部直接操作对象内部时,用备忘录模式。
二、备忘录模式的结构(三个角色)
| 角色 | 说明 |
|---|---|
| 原发器(Originator) | 拥有需要保存的“内部状态”;提供 create_memento() 生成当前状态的备忘录,提供 restore(memento) 从备忘录恢复状态。只有原发器知道备忘录里存了什么、怎么恢复。 |
| 备忘录(Memento) | 存放原发器在某一时刻的状态快照;对外可以“不透明”(管理者只存、取,不解析内容),或只暴露原发器需要的接口,避免其他对象修改快照。 |
| 管理者(Caretaker) | 负责保存备忘录(如放进列表做历史、写入文件);在需要恢复时把备忘录交还给原发器,由原发器 restore(memento)。管理者不修改备忘录内容,也不依赖原发器内部结构。 |
关系可以理解为:
- 原发器在关键点(如每次编辑后)调用 create_memento(),得到备忘录并交给管理者。
- 需要恢复时,管理者把某个备忘录还给原发器,原发器 restore(memento) 把自己的状态还原。
- 备忘录里具体存什么(字典、序列化字符串、私有属性),由原发器决定;管理者只负责“保管”,不解析。
三、示例一:简单计数器 + 撤销(最简版)
需求:一个计数器,有 value;可以 increase(),也可以“撤销”到上一步。用备忘录保存每次操作前的 value,撤销时恢复。
3.1 备忘录:只存一个数字(状态快照)
class Memento:
"""备忘录:只保存 value 的快照,对外可只读"""
def __init__(self, value: int):
self._value = value
def get_state(self):
return self._value
3.2 原发器:计数器(创建备忘录 + 恢复)
class Counter:
"""原发器:拥有状态,能创建备忘录并能从备忘录恢复"""
def __init__(self):
self.value = 0
def increase(self):
self.value += 1
def create_memento(self) -> Memento:
return Memento(self.value)
def restore(self, memento: Memento):
self.value = memento.get_state()
3.3 管理者:保存历史备忘录
class Caretaker:
"""管理者:只保存备忘录,不解析内容;需要时交还原发器"""
def __init__(self):
self._history = []
def save(self, memento: Memento):
self._history.append(memento)
def undo(self) -> Memento:
if not self._history:
return None
return self._history.pop()
3.4 使用
counter = Counter()
caretaker = Caretaker()
counter.increase() # 1
caretaker.save(counter.create_memento())
counter.increase() # 2
caretaker.save(counter.create_memento())
counter.increase() # 3
print(counter.value) # 3
m = caretaker.undo()
if m:
counter.restore(m)
print(counter.value) # 2
m = caretaker.undo()
if m:
counter.restore(m)
print(counter.value) # 1
要点:状态只存在 Memento 里,由 Counter 自己写入和读出;Caretaker 只做“存”和“取”,不关心 value 是多少。
四、示例二:文本编辑器(保存/恢复多字段)
需求:编辑器有 text 和 cursor_pos;支持“保存快照”和“从快照恢复”,用于撤销。
4.1 备忘录:存文本和光标(可做成不透明)
class EditorMemento:
"""备忘录:存编辑器某时刻的 text 和 cursor_pos"""
def __init__(self, text: str, cursor_pos: int):
self._text = text
self._cursor_pos = cursor_pos
def get_text(self):
return self._text
def get_cursor_pos(self):
return self._cursor_pos
4.2 原发器:编辑器
class Editor:
def __init__(self):
self.text = ""
self.cursor_pos = 0
def type_char(self, c: str):
self.text = self.text[:self.cursor_pos] + c + self.text[self.cursor_pos:]
self.cursor_pos += 1
def create_memento(self) -> EditorMemento:
return EditorMemento(self.text, self.cursor_pos)
def restore(self, m: EditorMemento):
self.text = m.get_text()
self.cursor_pos = m.get_cursor_pos()
4.3 管理者 + 使用
class History:
def __init__(self):
self._stack = []
def push(self, memento):
self._stack.append(memento)
def pop(self):
return self._stack.pop() if self._stack else None
editor = Editor()
history = History()
history.push(editor.create_memento()) # 空状态
editor.type_char("a")
editor.type_char("b")
history.push(editor.create_memento())
editor.type_char("c")
print(editor.text) # abc
m = history.pop()
editor.restore(m)
print(editor.text) # ab
要点:多字段(text、cursor_pos)都放进备忘录;恢复时由 Editor 自己从备忘录读出来写回自己,管理者不解析。
五、示例三:备忘录“不透明”(管理者不解析内容)
若希望管理者完全不能看到或修改备忘录内容,可以让备忘录只暴露给原发器:例如备忘录和原发器在同一模块,备忘录的“取状态”方法只允许原发器调用,或备忘录用私有属性存储,不提供 getter。Python 里可以用约定:备忘录类只由原发器创建和读取,管理者只持有引用。下面用“原发器把状态序列化成字典、备忘录只存字典”的方式,管理者拿到的只是“一个盒子”,不解析:
class Memento:
"""不透明备忘录:只存一个 dict,不提供按字段访问"""
def __init__(self, state: dict):
self._state = state.copy()
def get_state_for_originator(self):
"""仅原发器应调用,用于恢复"""
return self._state.copy()
class Originator:
def __init__(self):
self.x = 0
self.y = 0
def create_memento(self) -> Memento:
return Memento({"x": self.x, "y": self.y})
def restore(self, m: Memento):
state = m.get_state_for_originator()
self.x = state["x"]
self.y = state["y"]
管理者只做 caretaker.save(originator.create_memento()) 和 m = caretaker.undo(); originator.restore(m),不调用 get_state_for_originator,即不解析内容,实现“不透明”。
六、备忘录模式 vs 其他
| 模式 | 目的 | 与备忘录的区别 |
|---|---|---|
| 备忘录 | 保存与恢复对象状态,不破坏封装 | 强调“快照”“恢复”“管理者不解析”。 |
| 命令 + 撤销 | 用命令对象记录操作,撤销时执行反向操作 | 命令模式撤销靠“反向操作”;备忘录靠“恢复旧状态”。可结合:命令里存一个 memento,撤销时 restore。 |
| 原型 | 克隆对象得到副本 | 克隆是“复制当前对象”;备忘录是“存一份状态、以后恢复”,通常不保留对象引用。 |
简单记忆:备忘录 = 状态快照 + 原发器自己写/读 + 管理者只保管。
七、常见问题与注意点
7.1 备忘录里存什么?
存“恢复所需的最小状态”。可以是原发器所有相关字段的拷贝,或序列化后的字符串/字典;只有原发器知道如何生成和解析,管理者不依赖这些字段含义。
7.2 历史很多时会不会占内存?
会。可以限制历史条数(如只保留最近 20 条),或把旧备忘录持久化到磁盘,需要时再加载;备忘录模式本身不限制“存多少、存哪”,由管理者策略决定。
7.3 原发器变了(新增字段),旧备忘录怎么兼容?
若备忘录里是“字段名 -> 值”的字典,恢复时可以对缺失的键给默认值;若备忘录是序列化字节,可能需要版本号或兼容性策略。这是设计时的取舍问题。
八、小结
- 备忘录模式:在不破坏封装的前提下,把对象的状态保存成备忘录(快照);原发器负责创建备忘录和从备忘录恢复,管理者只负责保存和交出备忘录,不解析内容,从而支持撤销、存档、快照。
- 三个角色:原发器(有状态,create_memento / restore)、备忘录(存状态,可对管理者不透明)、管理者(保存/取出备忘录)。
- 典型用途:编辑器撤销、游戏存档、配置快照与回滚等。
- 实现要点:备忘录只暴露原发器需要的信息(或完全不暴露);恢复逻辑只在原发器里;管理者不修改、不解析备忘录。
建议先写“计数器 + 撤销”,再写“编辑器 text/cursor 快照与恢复”,体会“状态打包成备忘录、管理者只保管”,这样对备忘录模式会掌握得比较扎实。