20、Python - 备忘录模式

20、Python - 备忘录模式

你有没有遇到过这样的场景:写了一下午的文档,手滑按了删除键,几千字瞬间消失;玩游戏打到最终Boss前忘记存档,被Boss秒杀后只能从头再来;线上系统执行了一个错误的批量操作,数据被改得面目全非,却无法一键回滚。

如果这些场景让你感到痛苦,那么备忘录模式就是你的"后悔药"。它能在不破坏对象封装的前提下,随时保存对象的状态快照,并在需要时精确恢复到任意历史状态。

本文将从痛点出发,带你彻底搞懂备忘录模式的原理、实现、企业级应用以及面试考点,让小白也能看懂并直接上手。

文章目录

一、痛点场景描述

场景一:文本编辑器的噩梦

假设你正在开发一个在线文档编辑器,用户要求支持撤销功能。最朴素的做法是什么?每次用户输入时,把整个文档内容复制一份存到列表里,撤销时就取上一份覆盖回去。

python 复制代码
# 反例:朴素的撤销实现
class NaiveEditor:
    def __init__(self):
        self.content = ""
        self.history = []  # 存储所有历史内容

    def write(self, text):
        self.history.append(self.content)  # 每次修改前保存完整内容
        self.content += text

    def undo(self):
        if self.history:
            self.content = self.history.pop()

看起来没问题?但仔细想想:如果文档有10万字,用户每敲一个字就存一份完整副本,操作1000次就是1亿字的内存占用。而且,如果文档对象里还有光标位置、选中文本、格式化样式等十几个属性,难道要一个个手动复制吗?

更严重的是,为了保存状态,你不得不把所有内部属性都暴露成public,让外部代码可以读写,这直接破坏了面向对象的封装性。

场景二:游戏存档的困境

开发一款RPG游戏,玩家需要在关键节点存档。游戏角色的状态极其复杂:生命值、魔法值、装备列表、背包物品、任务进度、地图坐标、技能冷却......加起来几十个字段。

如果让外部系统直接读取和写入这些状态,不仅代码混乱,而且很容易出现"只恢复了一半状态"的bug。比如恢复了生命值却忘了恢复装备,导致玩家穿着不存在的装备战斗。

场景三:企业系统的回滚需求

在电商订单系统中,用户提交订单后可以在支付前修改订单。如果修改后发现不对,需要撤销修改回到之前的状态。订单对象包含商品列表、收货地址、优惠券、价格计算结果等复杂嵌套结构,简单的字段赋值根本无法正确回滚。

这些场景的共同痛点是:需要保存和恢复对象的完整状态,但又不想破坏封装性,也不想手动处理大量字段的复制逻辑。

二、痛点的解决方案

面对上述痛点,备忘录模式给出了优雅的解决方案:

  1. 谁的状态谁负责:由对象自己(Originator发起人)创建自己的状态快照(Memento备忘录),外部不需要知道对象内部有哪些字段。
  2. 快照独立封装:备忘录对象封装了状态的所有细节,对外只暴露一个"黑盒"引用,外部无法读取或修改其中的内容。
  3. 管理者统一保管:由专门的管理者(Caretaker)负责保存和取出备忘录,它只负责存储,不关心备忘录里装了什么。
  4. 恢复走原路径:恢复状态时,把备忘录交还给发起人,由发起人自己从备忘录中读取状态并恢复,确保恢复逻辑的正确性。

用大白话来说:备忘录模式就像你去银行租保险箱。你(发起人)把贵重物品(状态)装进箱子(备忘录)锁好,银行(管理者)只负责帮你保管箱子,它没有钥匙也不知道里面装了什么。需要取用时,你拿自己的钥匙打开箱子,东西原封不动地拿出来。

三、备忘录模式是什么

3.1 官方定义

备忘录模式(Memento Pattern),又叫快照模式(Snapshot Pattern)或Token模式,是GoF(四人帮)定义的23种设计模式之一,属于行为型模式。

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。

3.2 核心角色

备忘录模式包含三个核心角色:

角色 英文名 职责
发起人 Originator 创建当前状态的备忘录,也可以从备忘录恢复状态
备忘录 Memento 存储发起人对象的内部状态,防止外部访问
管理者 Caretaker 负责保存备忘录对象,但不操作也不检查备忘录内容

3.3 大白话解释

  • 发起人:就是那个需要保存状态的对象,比如文档编辑器、游戏角色、订单。它知道自己有哪些状态需要保存,也知道怎么从快照中恢复。
  • 备忘录:就是那个装着状态快照的"盒子"。它本身很简单,就是把状态存起来,而且只有发起人能打开它。
  • 管理者:就是那个"储物柜"。它把一个个备忘录按顺序存起来,需要的时候取出来。它从来不打开备忘录看里面是什么。

3.4 生活案例

生活中最贴切的例子就是游戏存档

  • 你的游戏角色就是发起人,它拥有生命值、装备、等级等状态。
  • 当你按下"存档"按钮,游戏系统把当前所有状态写入一个存档文件,这个存档文件就是备忘录
  • 你的硬盘或云存档服务就是管理者,它负责保存这些存档文件,但不会去修改存档内容。
  • 当你游戏失败选择"读档"时,游戏从存档文件中读取状态,恢复到存档时的样子。

另一个例子是Photoshop的历史记录面板:每一步操作都会生成一个画布状态的快照,存在历史记录列表里,点击任意一条就能恢复到那个状态。

四、为什么要用备忘录模式

4.1 解决了什么问题

备忘录模式主要解决以下问题:

  1. 封装性问题:不需要暴露对象的内部属性就能保存和恢复状态。对象的字段可以保持private,外部代码无法直接访问。
  2. 状态一致性问题:由对象自己负责保存和恢复,确保所有相关字段被一起保存和恢复,不会出现"恢复了一半"的情况。
  3. 历史管理问题:可以方便地保存多个历史状态,支持多级撤销、时间线回溯等功能。
  4. 关注点分离:状态的保存/恢复逻辑与业务逻辑分离,管理者只负责存储,职责清晰。

4.2 优点

  • 提供恢复机制:用户可以方便地回到任意历史状态,支持撤销、回滚、重做等功能。
  • 封装性良好:备忘录对象对外部是"黑盒",只有发起人能访问其内部状态,保护了对象的封装性。
  • 简化发起人:状态管理的职责交给了管理者,发起人不需要维护复杂的历史记录逻辑。
  • 支持多状态点:可以保存任意多个状态快照,实现多级撤销和版本管理。

4.3 缺点

  • 内存消耗大:如果对象状态很大,每次保存都会占用较多内存。频繁保存可能导致内存溢出。
  • 类数量增加:需要为每个发起人创建对应的备忘录类,增加了代码的复杂度。
  • 深拷贝开销:如果状态包含复杂的嵌套对象,需要进行深拷贝,可能带来性能开销。

4.4 什么时候用,什么时候不用

适用场景:

  • 需要实现撤销/重做(Undo/Redo)功能
  • 需要保存对象的快照以便后续恢复
  • 需要实现事务的回滚机制
  • 需要保护对象状态的封装性,不希望外部直接访问内部状态

不适用场景:

  • 对象状态非常大,保存快照的内存成本过高
  • 状态变化极其频繁,每次变化都保存快照不现实
  • 只需要简单的字段回退,不需要完整的状态快照

五、备忘录模式是怎么演进过来的

5.1 起源:GoF的经典定义

备忘录模式最早由Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides四人(俗称"四人帮"GoF)在1995年出版的《设计模式:可复用面向对象软件的基础》一书中正式定义。

在这本书中,备忘录模式被归类为行为型模式,其核心思想是:在不破坏封装的前提下捕获和外部化对象的内部状态,使对象可以在以后恢复到这个状态。

GoF提出这个模式的背景是:当时很多软件系统需要实现撤销功能,但常见的做法要么是暴露对象内部状态(破坏封装),要么是用命令模式记录操作反向执行(复杂且不通用)。备忘录模式提供了第三种选择------直接保存状态快照。

5.2 发展:从桌面软件到游戏行业

2000年代,随着桌面软件的普及,备忘录模式在文本编辑器、图形设计软件中得到广泛应用。Microsoft Word的撤销功能、Adobe Photoshop的历史记录面板,背后都有备忘录模式的思想。

2010年代,游戏行业爆发,存档/读档成为游戏的标配功能。备忘录模式的思想被大量应用于游戏引擎中,用于保存玩家进度、实现检查点系统。很多游戏引擎甚至内置了状态序列化和快照机制。

5.3 成熟:企业级应用与云原生

2020年代以来,备忘录模式在企业级系统中扮演越来越重要的角色:

  • 工作流引擎:审批流程中的撤回、回退功能,本质上就是保存流程实例的状态快照并恢复。
  • 分布式事务:Saga模式中的补偿操作,很多时候需要保存事务参与者的状态以便回滚。
  • 配置中心:配置版本管理和回滚功能,保存每个版本的配置快照。
  • 云原生:容器的快照、虚拟机的检查点(Checkpoint),都是备忘录模式思想在基础设施层的体现。

5.4 Python中的实践演进

在Python中,备忘录模式的实现也经历了演进:

  • 早期:手动实现三个角色,每个类都要写save和restore方法。
  • 进阶 :利用copy.deepcopy实现通用的深拷贝快照,减少重复代码。
  • 现代 :结合dataclasstyping等特性实现类型安全的备忘录,或者用pickle序列化实现状态的持久化存储。

六、备忘录模式怎么用

6.1 标准实现(三角色完整版本)

我们以文本编辑器为例,实现标准的备忘录模式。

python 复制代码
from typing import List, Optional


class Memento:
    """备忘录类:保存编辑器的状态快照"""

    def __init__(self, content: str, cursor_pos: int):
        # 使用私有属性存储状态,外部无法直接访问
        self._content = content
        self._cursor_pos = cursor_pos

    def get_content(self) -> str:
        """只有发起人能调用这个方法获取状态"""
        return self._content

    def get_cursor_pos(self) -> int:
        return self._cursor_pos


class TextEditor:
    """发起人类:文本编辑器,负责创建和恢复备忘录"""

    def __init__(self):
        self._content = ""
        self._cursor_pos = 0

    def write(self, text: str):
        """写入文本"""
        self._content = self._content[:self._cursor_pos] + text + self._content[self._cursor_pos:]
        self._cursor_pos += len(text)

    def delete(self, length: int = 1):
        """删除光标前的字符"""
        if self._cursor_pos >= length:
            self._content = self._content[:self._cursor_pos - length] + self._content[self._cursor_pos:]
            self._cursor_pos -= length

    def save(self) -> Memento:
        """创建当前状态的备忘录"""
        return Memento(self._content, self._cursor_pos)

    def restore(self, memento: Memento):
        """从备忘录恢复状态"""
        self._content = memento.get_content()
        self._cursor_pos = memento.get_cursor_pos()

    def show(self):
        """显示当前内容和光标位置"""
        print(f"内容: '{self._content}'")
        print(f"光标位置: {self._cursor_pos}")
        print("-" * 40)


class HistoryManager:
    """管理者类:负责保存和管理备忘录"""

    def __init__(self, max_size: int = 50):
        self._history: List[Memento] = []
        self._max_size = max_size

    def push(self, memento: Memento):
        """保存一个备忘录"""
        self._history.append(memento)
        # 限制历史记录数量,防止内存溢出
        if len(self._history) > self._max_size:
            self._history.pop(0)

    def pop(self) -> Optional[Memento]:
        """取出最近的备忘录(撤销)"""
        if self._history:
            return self._history.pop()
        return None

    def size(self) -> int:
        return len(self._history)


# 使用示例
if __name__ == "__main__":
    editor = TextEditor()
    history = HistoryManager()

    # 第一次编辑
    editor.write("Hello")
    history.push(editor.save())
    editor.show()

    # 第二次编辑
    editor.write(" World")
    history.push(editor.save())
    editor.show()

    # 删除操作
    editor.delete(5)
    editor.show()

    # 撤销一次
    memento = history.pop()
    if memento:
        editor.restore(memento)
    editor.show()

    # 再撤销一次
    memento = history.pop()
    if memento:
        editor.restore(memento)
    editor.show()

运行输出:

text 复制代码
内容: 'Hello'
光标位置: 5
----------------------------------------
内容: 'Hello World'
光标位置: 11
----------------------------------------
内容: 'Hello '
光标位置: 6
----------------------------------------
内容: 'Hello World'
光标位置: 11
----------------------------------------
内容: 'Hello'
光标位置: 5
----------------------------------------

6.2 Pythonic简化版(利用深拷贝)

Python中可以利用copy.deepcopy实现更简洁的通用备忘录,不需要为每个类手写备忘录类。

python 复制代码
import copy
from typing import List, Optional, Any


class Snapshot:
    """通用备忘录:保存对象的深拷贝快照"""

    def __init__(self, state: dict):
        # 深拷贝确保快照不受后续修改影响
        self._state = copy.deepcopy(state)

    def get_state(self) -> dict:
        return self._state


class OriginatorMixin:
    """发起人混入类:为任意对象添加保存和恢复能力"""

    def save(self) -> Snapshot:
        """保存当前所有属性的快照"""
        return Snapshot(self.__dict__.copy())

    def restore(self, snapshot: Snapshot):
        """从快照恢复所有属性"""
        self.__dict__.clear()
        self.__dict__.update(snapshot.get_state())


class GameCharacter(OriginatorMixin):
    """游戏角色:继承混入类即可获得存档能力"""

    def __init__(self, name: str):
        self.name = name
        self.hp = 100
        self.mp = 50
        self.level = 1
        self.inventory = ["新手剑", "红药水"]
        self.position = {"x": 0, "y": 0}

    def take_damage(self, damage: int):
        self.hp = max(0, self.hp - damage)

    def use_mp(self, amount: int):
        self.mp = max(0, self.mp - amount)

    def level_up(self):
        self.level += 1
        self.hp += 20
        self.mp += 10

    def move(self, x: int, y: int):
        self.position["x"] = x
        self.position["y"] = y

    def show(self):
        print(f"[{self.name}] Lv.{self.level} HP:{self.hp} MP:{self.mp}")
        print(f"  背包: {self.inventory}")
        print(f"  位置: ({self.position['x']}, {self.position['y']})")
        print("-" * 50)


class SaveManager:
    """存档管理器"""

    def __init__(self):
        self._saves: List[Snapshot] = []

    def save_game(self, character: GameCharacter):
        self._saves.append(character.save())
        print(f"已存档,当前共有 {len(self._saves)} 个存档")

    def load_game(self, character: GameCharacter, index: int = -1):
        if 0 <= abs(index) <= len(self._saves):
            character.restore(self._saves[index])
            print("读档成功")
        else:
            print("存档不存在")


# 使用示例
if __name__ == "__main__":
    player = GameCharacter("勇者小明")
    save_manager = SaveManager()

    player.show()

    # 第一个存档点
    save_manager.save_game(player)

    # 游戏进行中
    player.move(10, 20)
    player.take_damage(30)
    player.level_up()
    player.inventory.append("屠龙刀")
    player.show()

    # 第二个存档点
    save_manager.save_game(player)

    # 继续游戏,被Boss秒杀
    player.take_damage(100)
    player.show()

    # 读档回到第二个存档点
    save_manager.load_game(player)
    player.show()

    # 读档回到第一个存档点
    save_manager.load_game(player, 0)
    player.show()

6.3 支持撤销和重做(双栈实现)

很多场景需要同时支持撤销(Undo)和重做(Redo),可以用两个栈来实现。

python 复制代码
from typing import List, Optional


class UndoRedoManager:
    """支持撤销和重做的管理者"""

    def __init__(self, max_history: int = 100):
        self._undo_stack: List[Any] = []
        self._redo_stack: List[Any] = []
        self._max_history = max_history

    def record(self, memento: Any):
        """记录新状态,清空重做栈"""
        self._undo_stack.append(memento)
        self._redo_stack.clear()  # 新操作后之前的重做记录失效
        if len(self._undo_stack) > self._max_history:
            self._undo_stack.pop(0)

    def undo(self) -> Optional[Any]:
        """撤销:返回上一个状态"""
        if self._undo_stack:
            memento = self._undo_stack.pop()
            self._redo_stack.append(memento)
            return memento
        return None

    def redo(self) -> Optional[Any]:
        """重做:返回被撤销的状态"""
        if self._redo_stack:
            memento = self._redo_stack.pop()
            self._undo_stack.append(memento)
            return memento
        return None

    def can_undo(self) -> bool:
        return len(self._undo_stack) > 0

    def can_redo(self) -> bool:
        return len(self._redo_stack) > 0

6.4 持久化备忘录(保存到文件)

有时候需要把备忘录持久化到磁盘,比如游戏存档、文档自动保存。Python中可以用pickle序列化。

python 复制代码
import pickle
import os
from typing import Optional


class PersistentMemento:
    """可持久化的备忘录"""

    def __init__(self, state: dict):
        self._state = state

    def get_state(self) -> dict:
        return self._state

    def save_to_file(self, filepath: str):
        """将备忘录保存到文件"""
        with open(filepath, 'wb') as f:
            pickle.dump(self._state, f)

    @classmethod
    def load_from_file(cls, filepath: str) -> Optional['PersistentMemento']:
        """从文件加载备忘录"""
        if not os.path.exists(filepath):
            return None
        with open(filepath, 'rb') as f:
            state = pickle.load(f)
        return cls(state)

七、企业项目中如何使用

7.1 电商订单系统的订单修改与撤销

在电商系统中,用户提交订单后可以在支付前修改订单信息(如更换商品、修改收货地址)。如果用户修改后想撤销修改,需要恢复到修改前的状态。

python 复制代码
import copy
import uuid
from datetime import datetime
from typing import List, Dict, Optional


class OrderMemento:
    """订单备忘录:保存订单的完整状态"""

    def __init__(self, order_id: str, items: List[Dict], address: str,
                 coupon_id: Optional[str], total_price: float, status: str):
        self.order_id = order_id
        self.items = copy.deepcopy(items)
        self.address = address
        self.coupon_id = coupon_id
        self.total_price = total_price
        self.status = status
        self.snapshot_time = datetime.now()


class Order:
    """订单类(发起人)"""

    def __init__(self, user_id: str):
        self.order_id = str(uuid.uuid4())
        self.user_id = user_id
        self.items: List[Dict] = []
        self.address = ""
        self.coupon_id: Optional[str] = None
        self.total_price = 0.0
        self.status = "pending"  # pending, paid, shipped, completed, cancelled
        self.created_at = datetime.now()

    def add_item(self, product_id: str, name: str, price: float, quantity: int):
        """添加商品"""
        for item in self.items:
            if item["product_id"] == product_id:
                item["quantity"] += quantity
                break
        else:
            self.items.append({
                "product_id": product_id,
                "name": name,
                "price": price,
                "quantity": quantity
            })
        self._recalculate_price()

    def remove_item(self, product_id: str):
        """移除商品"""
        self.items = [item for item in self.items if item["product_id"] != product_id]
        self._recalculate_price()

    def update_address(self, address: str):
        """更新收货地址"""
        self.address = address

    def apply_coupon(self, coupon_id: str, discount: float):
        """使用优惠券"""
        self.coupon_id = coupon_id
        self.total_price = max(0, self.total_price - discount)

    def _recalculate_price(self):
        """重新计算总价"""
        self.total_price = sum(item["price"] * item["quantity"] for item in self.items)

    def save_snapshot(self) -> OrderMemento:
        """创建订单快照"""
        return OrderMemento(
            order_id=self.order_id,
            items=self.items,
            address=self.address,
            coupon_id=self.coupon_id,
            total_price=self.total_price,
            status=self.status
        )

    def restore_snapshot(self, memento: OrderMemento):
        """从快照恢复订单"""
        self.items = copy.deepcopy(memento.items)
        self.address = memento.address
        self.coupon_id = memento.coupon_id
        self.total_price = memento.total_price
        self.status = memento.status

    def show(self):
        print(f"订单号: {self.order_id}")
        print(f"状态: {self.status}")
        print(f"收货地址: {self.address}")
        print(f"优惠券: {self.coupon_id}")
        print(f"总价: ¥{self.total_price:.2f}")
        print("商品列表:")
        for item in self.items:
            print(f"  - {item['name']} x{item['quantity']} = ¥{item['price'] * item['quantity']:.2f}")
        print("=" * 50)


class OrderHistoryManager:
    """订单历史管理器(管理者)"""

    def __init__(self):
        self._snapshots: Dict[str, List[OrderMemento]] = {}

    def save_snapshot(self, order: Order):
        """保存订单快照"""
        if order.order_id not in self._snapshots:
            self._snapshots[order.order_id] = []
        self._snapshots[order.order_id].append(order.save_snapshot())
        print(f"订单 {order.order_id} 已保存快照,共 {len(self._snapshots[order.order_id])} 个历史版本")

    def undo(self, order: Order) -> bool:
        """撤销到上一个版本"""
        snapshots = self._snapshots.get(order.order_id, [])
        if len(snapshots) >= 2:
            snapshots.pop()  # 移除当前版本
            previous = snapshots[-1]  # 取上一个版本
            order.restore_snapshot(previous)
            print("撤销成功")
            return True
        print("没有可撤销的历史版本")
        return False

    def get_history(self, order_id: str) -> List[OrderMemento]:
        return self._snapshots.get(order_id, [])


# 企业项目使用示例
if __name__ == "__main__":
    # 创建订单
    order = Order(user_id="user_1001")
    history_manager = OrderHistoryManager()

    # 添加商品
    order.add_item("P001", "机械键盘", 299.0, 1)
    order.add_item("P002", "鼠标垫", 29.9, 2)
    order.update_address("北京市朝阳区xxx街道")
    history_manager.save_snapshot(order)  # 快照1:初始订单
    order.show()

    # 用户修改订单:添加显示器
    order.add_item("P003", "27寸显示器", 1599.0, 1)
    history_manager.save_snapshot(order)  # 快照2:添加显示器后
    order.show()

    # 用户使用优惠券
    order.apply_coupon("COUPON_100", 100.0)
    history_manager.save_snapshot(order)  # 快照3:使用优惠券后
    order.show()

    # 用户觉得显示器太贵,撤销修改
    history_manager.undo(order)  # 回到快照2
    order.show()

    # 再撤销一次,回到初始订单
    history_manager.undo(order)  # 回到快照1
    order.show()

7.2 工作流引擎中的审批撤回

在OA审批系统中,审批人可以撤回已经提交的审批,回到上一个节点。这本质上就是保存流程实例的状态快照。

python 复制代码
from enum import Enum
from typing import List, Dict, Any
import copy


class ProcessStatus(Enum):
    DRAFT = "草稿"
    PENDING = "审批中"
    APPROVED = "已通过"
    REJECTED = "已拒绝"
    WITHDRAWN = "已撤回"


class ProcessMemento:
    """流程实例备忘录"""

    def __init__(self, status: ProcessStatus, current_node: str,
                 approvers: List[str], approval_history: List[Dict],
                 form_data: Dict[str, Any]):
        self.status = status
        self.current_node = current_node
        self.approvers = copy.deepcopy(approvers)
        self.approval_history = copy.deepcopy(approval_history)
        self.form_data = copy.deepcopy(form_data)


class WorkflowInstance:
    """工作流实例(发起人)"""

    def __init__(self, process_id: str, applicant: str):
        self.process_id = process_id
        self.applicant = applicant
        self.status = ProcessStatus.DRAFT
        self.current_node = "start"
        self.approvers: List[str] = []
        self.approval_history: List[Dict] = []
        self.form_data: Dict[str, Any] = {}

    def submit(self, form_data: Dict[str, Any]):
        """提交审批"""
        self.form_data = form_data
        self.status = ProcessStatus.PENDING
        self.current_node = "manager_approval"
        self.approvers = ["manager_001"]

    def approve(self, approver: str, comment: str = ""):
        """审批通过"""
        self.approval_history.append({
            "approver": approver,
            "action": "approve",
            "comment": comment
        })
        if self.current_node == "manager_approval":
            self.current_node = "director_approval"
            self.approvers = ["director_001"]
        elif self.current_node == "director_approval":
            self.status = ProcessStatus.APPROVED
            self.current_node = "end"
            self.approvers = []

    def reject(self, approver: str, comment: str = ""):
        """审批拒绝"""
        self.approval_history.append({
            "approver": approver,
            "action": "reject",
            "comment": comment
        })
        self.status = ProcessStatus.REJECTED
        self.current_node = "end"

    def save_snapshot(self) -> ProcessMemento:
        return ProcessMemento(
            status=self.status,
            current_node=self.current_node,
            approvers=self.approvers,
            approval_history=self.approval_history,
            form_data=self.form_data
        )

    def restore_snapshot(self, memento: ProcessMemento):
        self.status = memento.status
        self.current_node = memento.current_node
        self.approvers = copy.deepcopy(memento.approvers)
        self.approval_history = copy.deepcopy(memento.approval_history)
        self.form_data = copy.deepcopy(memento.form_data)

    def show(self):
        print(f"流程: {self.process_id}")
        print(f"状态: {self.status.value}")
        print(f"当前节点: {self.current_node}")
        print(f"当前审批人: {self.approvers}")
        print(f"审批历史: {len(self.approval_history)} 条")
        print("-" * 40)


class WorkflowHistory:
    """工作流历史管理器"""

    def __init__(self):
        self._history: Dict[str, List[ProcessMemento]] = {}

    def record(self, instance: WorkflowInstance):
        if instance.process_id not in self._history:
            self._history[instance.process_id] = []
        self._history[instance.process_id].append(instance.save_snapshot())

    def withdraw(self, instance: WorkflowInstance) -> bool:
        """撤回到上一个状态"""
        history = self._history.get(instance.process_id, [])
        if len(history) >= 2:
            history.pop()
            instance.restore_snapshot(history[-1])
            print("撤回成功")
            return True
        print("无法撤回")
        return False


# 使用示例
if __name__ == "__main__":
    wf = WorkflowInstance("WF_001", "zhangsan")
    history = WorkflowHistory()

    # 提交申请
    wf.submit({"title": "出差申请", "days": 3})
    history.record(wf)
    wf.show()

    # 经理审批通过
    wf.approve("manager_001", "同意")
    history.record(wf)
    wf.show()

    # 发现有误,撤回
    history.withdraw(wf)
    wf.show()

7.3 配置中心的版本管理与回滚

在微服务架构中,配置中心需要支持配置版本管理和一键回滚。

python 复制代码
import json
import time
from typing import Dict, List, Optional


class ConfigSnapshot:
    """配置快照"""

    def __init__(self, version: int, config: Dict, operator: str):
        self.version = version
        self.config = config.copy()
        self.operator = operator
        self.timestamp = time.time()


class ConfigCenter:
    """配置中心(发起人)"""

    def __init__(self, app_name: str):
        self.app_name = app_name
        self._config: Dict[str, str] = {}
        self._version = 0

    def set(self, key: str, value: str):
        self._config[key] = value

    def delete(self, key: str):
        self._config.pop(key, None)

    def get(self, key: str) -> Optional[str]:
        return self._config.get(key)

    def get_all(self) -> Dict[str, str]:
        return self._config.copy()

    def publish(self, operator: str) -> ConfigSnapshot:
        """发布配置,生成快照"""
        self._version += 1
        return ConfigSnapshot(self._version, self._config, operator)

    def rollback(self, snapshot: ConfigSnapshot):
        """回滚到指定版本"""
        self._config = snapshot.config.copy()
        self._version = snapshot.version

    def show(self):
        print(f"[{self.app_name}] 当前版本: v{self._version}")
        for k, v in self._config.items():
            print(f"  {k} = {v}")
        print("-" * 40)


class ConfigVersionManager:
    """配置版本管理器"""

    def __init__(self):
        self._versions: Dict[str, List[ConfigSnapshot]] = {}

    def publish(self, config_center: ConfigCenter, operator: str):
        snapshot = config_center.publish(operator)
        if config_center.app_name not in self._versions:
            self._versions[config_center.app_name] = []
        self._versions[config_center.app_name].append(snapshot)
        print(f"配置发布成功,当前版本 v{snapshot.version}")

    def rollback_to(self, config_center: ConfigCenter, version: int) -> bool:
        versions = self._versions.get(config_center.app_name, [])
        for snapshot in versions:
            if snapshot.version == version:
                config_center.rollback(snapshot)
                print(f"已回滚到版本 v{version}")
                return True
        print(f"版本 v{version} 不存在")
        return False

    def list_versions(self, app_name: str) -> List[ConfigSnapshot]:
        return self._versions.get(app_name, [])


# 使用示例
if __name__ == "__main__":
    cc = ConfigCenter("order-service")
    vm = ConfigVersionManager()

    # v1 初始配置
    cc.set("db.host", "10.0.0.1")
    cc.set("db.port", "3306")
    cc.set("cache.ttl", "300")
    vm.publish(cc, "admin")
    cc.show()

    # v2 修改配置
    cc.set("db.host", "10.0.0.2")
    cc.set("cache.ttl", "600")
    cc.set("log.level", "debug")
    vm.publish(cc, "admin")
    cc.show()

    # v3 出问题了,回滚到v1
    vm.rollback_to(cc, 1)
    cc.show()

八、与最相关的几个竞品对比

备忘录模式、命令模式和原型模式都涉及"保存状态"或"复制对象"的概念,经常被混淆。下面从多个维度进行详细对比。

8.1 核心对比表

对比维度 备忘录模式 (Memento) 命令模式 (Command) 原型模式 (Prototype)
核心目的 保存和恢复对象状态 将请求封装为对象,支持操作的参数化、队列化和撤销 通过克隆已有对象创建新对象
是否保存状态 是,主动保存完整状态快照 可选,命令对象可存储操作前的部分状态 是,创建对象的完整副本
撤销实现方式 直接恢复状态快照 通过执行反向操作(undo方法) 不直接支持撤销
状态可见性 备忘录对外部是黑盒,只有发起人能访问 命令对象的状态通常是公开的 克隆对象的状态完全公开
典型场景 文本编辑器撤销、游戏存档、事务回滚 菜单按钮、操作队列、宏命令、多级撤销 创建成本高的对象、批量创建相似对象
内存消耗 高,每个快照保存完整状态 中,只保存操作相关参数 高,每个克隆都是完整对象
类数量 每个发起人需要对应备忘录类 每个操作需要一个命令类 每个原型类需要实现克隆方法
优点 封装性好,恢复简单直接 解耦调用者和执行者,支持操作队列和日志 创建对象高效,避免重复初始化
缺点 内存消耗大,状态大时不适用 类数量膨胀,反向操作实现复杂 深拷贝实现复杂,嵌套对象需注意

8.2 详细辨析

备忘录模式 vs 命令模式:

两者都能实现撤销功能,但思路完全不同:

  • 备忘录模式是"状态派":我不管你做了什么操作,我直接把操作前的完整状态存下来,撤销时用旧状态覆盖新状态。简单粗暴,但内存消耗大。
  • 命令模式是"操作派":我把每一步操作封装成命令对象,每个命令对象知道自己怎么执行(execute),也知道怎么反向执行(undo)。撤销时不是恢复状态,而是执行反向操作。内存消耗小,但每个操作都要写反向逻辑。

举个例子:你在画板上画了一个红色的圆。

  • 备忘录模式:保存画圆之前整个画布的像素快照。撤销时直接用旧画布覆盖。
  • 命令模式:保存"在(x,y)位置画一个半径r的红色圆"这个命令。撤销时执行"在(x,y)位置擦除一个半径r的圆"。

备忘录模式 vs 原型模式:

两者都涉及对象复制,但目的不同:

  • 备忘录模式:复制是为了"保存",将来要恢复到原对象。复制出来的备忘录是被动存储的,不会被当作独立对象使用。
  • 原型模式:复制是为了"创建",复制出来的是一个全新的独立对象,可以被修改和使用,与原对象互不影响。

简单说:备忘录是"备份",原型是"克隆"。备份是为了还原,克隆是为了得到一个新个体。

8.3 如何选择

场景 推荐模式 原因
需要保存完整状态,状态结构复杂 备忘录模式 自动保存所有字段,不会遗漏
操作可逆,且反向操作容易实现 命令模式 内存消耗小,支持操作队列
需要创建大量相似对象,初始化成本高 原型模式 克隆比重新创建高效
同时需要撤销和操作队列 命令模式 + 备忘录模式 命令记录操作,备忘录保存关键状态
大对象的频繁撤销 命令模式 避免每次保存完整快照的内存开销

九、常用场景总结

9.1 文本编辑器的撤销/重做

这是备忘录模式最经典的应用场景。每输入一段文字或执行一个操作,就保存当前文档的状态快照到历史栈中。按下Ctrl+Z时弹出最近的快照恢复,Ctrl+Y则重做。

实际应用: Microsoft Word、Google Docs、Notion、飞书文档等所有现代文档编辑器。

9.2 游戏存档/读档系统

游戏角色的状态非常复杂(生命值、装备、背包、任务进度、位置等),备忘录模式可以一次性保存完整状态,读档时完整恢复。

实际应用: 单机游戏的存档系统、MMORPG的检查点、云存档服务。

9.3 数据库事务回滚

在数据库事务中,如果执行过程中出现错误,需要回滚到事务开始前的状态。数据库的Undo Log本质上就是备忘录模式的思想------保存修改前的数据以便回滚。

实际应用: MySQL的InnoDB引擎Undo Log、分布式事务Saga模式的补偿操作。

9.4 工作流审批撤回

在OA系统中,审批流程可以撤回,回到上一个审批节点。需要保存流程实例在每个节点的状态(当前节点、审批人、表单数据等)。

实际应用: 飞书审批、钉钉审批、企业OA系统。

9.5 图形设计软件的历史记录

Photoshop、Figma等设计软件支持多级撤销,每一步操作(画图形、改颜色、移动图层)都会生成画布状态的快照。

实际应用: Adobe Photoshop、Figma、Sketch、Canva。

9.6 配置中心版本管理

微服务配置中心需要保存每个版本的配置快照,支持一键回滚到任意历史版本。

实际应用: Nacos、Apollo、Spring Cloud Config。

9.7 代码版本控制

Git等版本控制系统的核心思想就是备忘录模式:每次commit保存整个代码库的状态快照,可以checkout到任意历史版本。

实际应用: Git、SVN、Mercurial。

9.8 虚拟机/容器快照

云计算中的虚拟机快照、容器检查点(Checkpoint)功能,保存整个运行时状态,支持快速恢复。

实际应用: VMware快照、Docker Checkpoint、AWS EC2 AMI。

十、面试官高频面试题

面试题1:备忘录模式有哪些核心角色?各自的职责是什么?

参考答案:

备忘录模式有三个核心角色:

  1. 发起人(Originator):需要保存状态的对象。负责创建一个包含当前内部状态的备忘录对象,也负责从备忘录中恢复状态。它是唯一能够访问备忘录内部状态的对象。

  2. 备忘录(Memento):存储发起人对象的内部状态。备忘录应该保护其内容不被发起人以外的对象访问。通常设计为不可变对象,构造时传入状态,只提供getter方法。

  3. 管理者(Caretaker):负责保存备忘录对象,但不对备忘录的内容进行操作或检查。它只负责存储和提供备忘录,不知道备忘录里装了什么。常见实现是用一个列表或栈来保存多个备忘录。

面试题2:如何保证备忘录的封装性不被破坏?

参考答案:

保证封装性的关键是让备忘录的内部状态只能被发起人访问,管理者和其他对象只能持有备忘录的引用,不能读取或修改其内容。具体手段包括:

  1. 将备忘录设计为不可变对象:构造函数传入状态后,不再提供setter方法,状态只能在创建时设置。

  2. 使用私有内部类:在Java等语言中,可以将备忘录类作为发起人的私有内部类,这样外部完全无法访问备忘录的类定义和方法。

  3. 窄接口和宽接口分离:对管理者提供窄接口(只有空接口或标记接口),对发起人提供宽接口(可以访问所有状态)。管理者只能通过窄接口持有备忘录引用。

  4. Python中的约定:Python没有真正的私有,但可以通过以下方式约定:

    • 属性名前加双下划线(如_state),触发名称改写
    • 备忘录类的方法文档中注明"仅发起人可调用"
    • 使用@property只提供getter,不提供setter
python 复制代码
# Python中保证封装性的示例
class Memento:
    def __init__(self, state):
        self.__state = state  # 双下划线名称改写

    def _get_state(self):  # 约定仅内部使用
        return self.__state

面试题3:备忘录模式和命令模式都能实现撤销,有什么区别?

参考答案:

两者实现撤销的思路完全不同:

维度 备忘录模式 命令模式
撤销原理 恢复之前保存的完整状态快照 执行操作的反向操作(undo)
内存消耗 高,每个快照保存完整状态 低,只保存操作参数
实现复杂度 低,保存和恢复状态即可 高,每个操作都要写反向逻辑
适用场景 状态结构复杂、反向操作难实现 操作可逆、状态变化大、频繁撤销
状态可见性 备忘录是黑盒,封装性好 命令对象状态通常公开

举例说明: 在文本编辑器中删除一段文字:

  • 备忘录模式:保存删除前的完整文档内容,撤销时用旧内容覆盖。
  • 命令模式:保存"在位置p删除长度为n的文字"这个命令,撤销时执行"在位置p插入被删除的文字"。

实际项目中,两者经常结合使用:命令模式负责记录操作和实现撤销逻辑,对于复杂操作(如滤镜效果),命令对象内部会用备忘录模式保存操作前的状态快照。

面试题4:备忘录模式有什么缺点?如何优化内存消耗?

参考答案:

主要缺点:

  1. 内存消耗大:如果对象状态很大,每次保存都会占用较多内存。保存大量快照可能导致内存溢出。
  2. 类数量增加:需要为每个发起人创建对应的备忘录类,增加代码复杂度。
  3. 深拷贝开销:状态包含复杂嵌套对象时,深拷贝有性能开销。

内存优化策略:

  1. 限制历史记录数量:设置最大保存数量,超过后删除最旧的快照(环形缓冲区)。
python 复制代码
class BoundedHistory:
    def __init__(self, max_size=20):
        self._history = []
        self._max_size = max_size

    def push(self, memento):
        self._history.append(memento)
        if len(self._history) > self._max_size:
            self._history.pop(0)  # 移除最旧的
  1. 增量快照:只保存与上一个状态的差异(delta),而不是完整状态。恢复时从基线状态依次应用差异。

  2. 状态压缩:对备忘录对象进行序列化和压缩(如使用gzip、zlib),用CPU时间换内存空间。

  3. 持久化存储:将不常用的历史快照写入磁盘或数据库,需要时再加载到内存。

  4. 引用共享:对于状态中不变的大对象(如图片、配置),使用引用共享而不是复制,只复制变化的部分(写时复制COW)。

  5. 选择合适的时机保存:不是每次状态变化都保存,而是在关键节点(如用户操作完成后、事务提交前)保存。

面试题5:深拷贝和浅拷贝在备忘录模式中如何选择?

参考答案:

这是一个非常重要的问题,选择错误会导致严重的bug。

浅拷贝(copy.copy):只复制对象本身,不复制内部引用的对象。原对象和拷贝对象共享内部子对象。

深拷贝(copy.deepcopy):递归复制对象及其所有引用的子对象。原对象和拷贝对象完全独立。

选择原则:

  • 如果状态只包含不可变对象(int、str、tuple、frozenset),浅拷贝即可,因为不可变对象无法被修改,共享引用没有风险。
  • 如果状态包含可变对象(list、dict、set、自定义对象),必须使用深拷贝。否则修改原对象的可变属性会影响备忘录中的状态,导致恢复时状态不正确。
python 复制代码
import copy

class GameCharacter:
    def __init__(self):
        self.hp = 100          # 不可变
        self.inventory = []    # 可变!
        self.position = {"x": 0, "y": 0}  # 可变!

    # 错误:浅拷贝
    def save_wrong(self):
        return copy.copy(self.__dict__)  # inventory和position还是共享的

    # 正确:深拷贝
    def save_correct(self):
        return copy.deepcopy(self.__dict__)  # 完全独立

深拷贝的注意事项:

  • 深拷贝会递归复制所有对象,对于包含循环引用的对象可能出问题(Python的deepcopy已经处理了循环引用)。
  • 深拷贝性能开销大,大对象要注意。
  • 某些对象不可拷贝(如文件句柄、网络连接、线程对象),深拷贝会报错,需要自定义__deepcopy__方法。

面试题6:请设计一个支持撤销和重做的文本编辑器

参考答案:

这是一个经典的系统设计题,完整的实现需要用到备忘录模式 + 双栈结构:

python 复制代码
import copy


class TextEditor:
    def __init__(self):
        self.content = ""
        self.cursor_pos = 0
        self._undo_stack = []
        self._redo_stack = []

    def _create_snapshot(self):
        """创建当前状态的快照"""
        return {
            "content": self.content,
            "cursor_pos": self.cursor_pos
        }

    def _restore_snapshot(self, snapshot):
        """从快照恢复状态"""
        self.content = snapshot["content"]
        self.cursor_pos = snapshot["cursor_pos"]

    def _record_state(self):
        """记录当前状态到撤销栈,清空重做栈"""
        self._undo_stack.append(self._create_snapshot())
        self._redo_stack.clear()
        # 限制历史数量
        if len(self._undo_stack) > 100:
            self._undo_stack.pop(0)

    def insert(self, text):
        self._record_state()
        self.content = self.content[:self.cursor_pos] + text + self.content[self.cursor_pos:]
        self.cursor_pos += len(text)

    def delete(self, length=1):
        self._record_state()
        if self.cursor_pos >= length:
            self.content = self.content[:self.cursor_pos - length] + self.content[self.cursor_pos:]
            self.cursor_pos -= length

    def undo(self):
        if self._undo_stack:
            # 当前状态存入重做栈
            self._redo_stack.append(self._create_snapshot())
            # 从撤销栈取出上一个状态
            snapshot = self._undo_stack.pop()
            self._restore_snapshot(snapshot)
            return True
        return False

    def redo(self):
        if self._redo_stack:
            # 当前状态存入撤销栈
            self._undo_stack.append(self._create_snapshot())
            # 从重做栈取出状态
            snapshot = self._redo_stack.pop()
            self._restore_snapshot(snapshot)
            return True
        return False

    def can_undo(self):
        return len(self._undo_stack) > 0

    def can_redo(self):
        return len(self._redo_stack) > 0

设计要点:

  1. 每次执行修改操作前,先保存当前状态到撤销栈。
  2. 新操作会清空重做栈,因为之前的重做路径已经失效。
  3. 撤销时,当前状态存入重做栈,然后从撤销栈弹出上一个状态恢复。
  4. 重做时,当前状态存入撤销栈,然后从重做栈弹出状态恢复。
  5. 限制历史记录数量,防止内存溢出。

十一、总结

备忘录模式是一个看似简单但非常实用的设计模式。它的核心思想只有一句话:在不破坏封装的前提下,保存对象的状态快照,以便后续恢复。

回顾本文的要点:

  1. 三个角色:发起人(Originator)创建和恢复状态,备忘录(Memento)存储状态,管理者(Caretaker)保管备忘录。
  2. 核心价值:封装性保护 + 状态一致性 + 历史管理。
  3. 适用场景:撤销重做、游戏存档、事务回滚、审批撤回、配置版本管理。
  4. 与命令模式的区别:备忘录是"恢复状态",命令是"反向执行"。
  5. 内存优化:限制数量、增量快照、状态压缩、持久化、写时复制。
  6. 深拷贝必须:状态包含可变对象时必须深拷贝,否则备忘录会被污染。

掌握备忘录模式,你就掌握了给程序"吃后悔药"的能力。无论是做编辑器、游戏还是企业系统,你都会发现它的用武之地。


转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
Kobebryant-Manba1 小时前
学习Bert微调
人工智能·学习·bert
我爱写代码i1 小时前
BeLink - 支持生成多种URL 缩短网址PHP源码
开发语言·php·短网址php源码
飞哥数智坊1 小时前
我对 AI 生图的一点工程化理解
人工智能·aigc
ACP广源盛139246256731 小时前
M6/M5 Pro Mac mini 端侧 AI 新形态@ACP#GSV5800 Serdes 长距离视频传输在 AI 服务中的机会与落地场景
大数据·网络·数据库·人工智能·嵌入式硬件·macos·音视频
xian_wwq1 小时前
【学习笔记】深度认知系列-第14讲 端侧AI崛起——为什么AI正在从云端走向本地
人工智能·笔记·学习
火山引擎开发者社区1 小时前
火山引擎 Milvus Vector Lakebase 正式公测
人工智能
ocean21031 小时前
2025-2026年AI部署与MLOps大厂面试高频问题
人工智能·面试·大模型推理·ai部署
嘿嘿-662 小时前
Windows 一键使用 GPT-6 Astra:Codex CLI 配置教程
java·人工智能·windows·gpt·chatgpt·web
2601_962077712 小时前
基于Python与NLP的新闻事件信息抽取实战:从NER到时空标准化
python·nlp·transformers·spacy·新闻事件抽取