备忘录模式(Memento Pattern)

C++ 备忘录模式(Memento Pattern)

一、模式基础概述

1.1 定义

备忘录模式属于行为型设计模式 ,在不破坏对象封装性 的前提下,捕获并保存一个对象的内部状态,后续可将对象恢复到之前保存的状态。常用来实现撤销、重做、存档、状态回滚等功能。

1.2 核心思想

  • 分离状态保存状态持有状态恢复三类职责;
  • 对象内部状态对外隐藏,仅自身可读写,保证数据安全;
  • 通过中间备忘录对象完成状态快照,负责人统一管理历史记录。

1.3 设计原则

遵循封装原则单一职责原则,不对外暴露原发器私有成员。


二、核心角色与职责

备忘录模式包含三大核心角色,角色间访问权限严格隔离:

角色名称 英文名称 核心职责 访问权限 关键方法
原发器 Originator 业务主体对象; 1. 创建备忘录,保存自身当前状态 2. 从备忘录恢复历史状态 可读写备忘录内部数据 createMemento() restore(Memento*)
备忘录 Memento 纯数据容器,仅存储原发器状态快照 原发器 可访问内部成员; 外部类无法创建、修改 私有成员变量(状态数据) 私有构造函数
负责人 Caretaker 管理备忘录集合; 1. 保存历史快照 2. 提供撤销、重做、清空历史功能 仅持有备忘录指针/对象,不能读取/修改内部状态 save() undo() redo() clearHistory()

2.1 角色协作流程

  1. 原发器调用 createMemento(),生成包含当前状态的备忘录对象;
  2. 负责人将备忘录存入历史列表,完成状态存档;
  3. 执行撤销/重做时,负责人取出对应备忘录;
  4. 原发器调用 restore(),依据备忘录数据还原自身状态;
  5. 全程负责人只做存储与调度,不触碰状态数据。

2.2 依赖关系

  • 原发器 ↔ 备忘录:友元关系,唯一合法访问通道;
  • 负责人 → 备忘录:仅持有引用,无访问内部权限;
  • 负责人 → 原发器:调用原发器方法完成存档与恢复。

三、核心实现要点(C++ 专属)

  1. 友元(friend)
    备忘录将原发器声明为友元,保证只有原发器能读写私有状态,严格保护封装。
  2. 私有构造函数
    备忘录构造函数设为 private,外部无法实例化,只能由原发器创建。
  3. 禁用拷贝/赋值
    防止备忘录对象被意外复制、篡改,使用 = delete 禁用拷贝构造与赋值运算符。
  4. 智能指针管理
    使用 std::unique_ptr / std::shared_ptr 管理备忘录生命周期,避免内存泄漏。
  5. 深拷贝
    若状态包含指针、容器等复杂结构,必须做深拷贝,避免浅拷贝引发数据错乱。

四、完整 C++ 代码实现

4.1 业务场景

模拟游戏角色状态存档/读档:角色拥有生命值、攻击力、位置坐标,实现存档、多步撤销回档功能。

cpp 复制代码
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>

// 前向声明
class GameRole;

// ===================== 1. 备忘录 Memento(状态快照) =====================
class RoleMemento
{
    // 友元:仅游戏角色可以访问内部状态
    friend class GameRole;
private:
    // 角色状态数据
    int hp_;        // 生命值
    int attack_;    // 攻击力
    double posX_;   // X坐标
    double posY_;   // Y坐标

    // 私有构造函数:外部无法创建备忘录
    RoleMemento(int hp, int atk, double x, double y)
        : hp_(hp), attack_(atk), posX_(x), posY_(y) {}

    // 禁用拷贝构造与赋值,防止外部篡改
    RoleMemento(const RoleMemento&) = delete;
    RoleMemento& operator=(const RoleMemento&) = delete;
};

// ===================== 2. 原发器 Originator(游戏角色) =====================
class GameRole
{
private:
    // 当前角色状态
    int hp_;
    int attack_;
    double posX_;
    double posY_;
public:
    // 构造:初始化角色属性
    GameRole(int hp, int atk, double x, double y)
        : hp_(hp), attack_(atk), posX_(x), posY_(y) {}

    // 角色行为:受到伤害
    void hurt(int damage)
    {
        hp_ = std::max(0, hp_ - damage);
        std::cout << "角色受到 " << damage << " 点伤害\n";
    }

    // 角色行为:移动位置
    void move(double x, double y)
    {
        posX_ = x;
        posY_ = y;
        std::cout << "角色移动到坐标 (" << x << ", " << y << ")\n";
    }

    // 创建备忘录:保存当前完整状态
    std::unique_ptr<RoleMemento> createMemento() const
    {
        return std::make_unique<RoleMemento>(hp_, attack_, posX_, posY_);
    }

    // 从备忘录恢复状态
    void restore(const RoleMemento* memento)
    {
        if (!memento) return;
        hp_ = memento->hp_;
        attack_ = memento->attack_;
        posX_ = memento->posX_;
        posY_ = memento->posY_;
    }

    // 打印当前角色状态
    void showStatus() const
    {
        std::cout << "【角色状态】生命值:" << hp_
                  << "  攻击力:" << attack_
                  << "  坐标:(" << posX_ << ", " << posY_ << ")\n\n";
    }
};

// ===================== 3. 负责人 Caretaker(存档管理器) =====================
class SaveManager
{
private:
    // 存储所有历史存档快照
    std::vector<std::unique_ptr<RoleMemento>> saveList_;
    int currentIndex_ = -1;     // 当前所处存档位置
    const int MAX_SAVE = 10;    // 最大存档数量,限制内存占用
public:
    // 保存当前状态为新存档
    void save(GameRole& role)
    {
        // 撤销后重新操作,删除后续无效存档
        if (currentIndex_ < (int)saveList_.size() - 1)
        {
            saveList_.erase(saveList_.begin() + currentIndex_ + 1, saveList_.end());
        }
        // 限制最大存档数
        if ((int)saveList_.size() >= MAX_SAVE)
        {
            saveList_.erase(saveList_.begin());
            currentIndex_--;
        }
        saveList_.push_back(role.createMemento());
        currentIndex_ = (int)saveList_.size() - 1;
        std::cout << ">>> 状态已存档\n";
    }

    // 撤销(回退到上一存档)
    bool undo(GameRole& role)
    {
        if (currentIndex_ <= 0)
        {
            std::cout << ">>> 无法撤销,已到最早记录\n";
            return false;
        }
        currentIndex_--;
        role.restore(saveList_[currentIndex_].get());
        std::cout << ">>> 撤销成功\n";
        return true;
    }

    // 重做(前进到下一存档)
    bool redo(GameRole& role)
    {
        if (currentIndex_ >= (int)saveList_.size() - 1)
        {
            std::cout << ">>> 无法重做,已到最新记录\n";
            return false;
        }
        currentIndex_++;
        role.restore(saveList_[currentIndex_].get());
        std::cout << ">>> 重做成功\n";
        return true;
    }

    // 清空所有存档
    void clearAll()
    {
        saveList_.clear();
        currentIndex_ = -1;
        std::cout << ">>> 所有存档已清空\n";
    }
};

// ===================== 客户端测试 =====================
int main()
{
    // 创建游戏角色 + 存档管理器
    GameRole hero(100, 50, 0.0, 0.0);
    SaveManager saveMgr;

    std::cout << "===== 初始状态 =====" << std::endl;
    hero.showStatus();
    saveMgr.save(hero);

    // 第一次操作:受伤
    hero.hurt(30);
    hero.showStatus();
    saveMgr.save(hero);

    // 第二次操作:移动位置
    hero.move(10.5, 20.8);
    hero.showStatus();
    saveMgr.save(hero);

    // 执行撤销
    std::cout << "===== 执行第一次撤销 =====" << std::endl;
    saveMgr.undo(hero);
    hero.showStatus();

    std::cout << "===== 执行第二次撤销 =====" << std::endl;
    saveMgr.undo(hero);
    hero.showStatus();

    // 执行重做
    std::cout << "===== 执行重做 =====" << std::endl;
    saveMgr.redo(hero);
    hero.showStatus();

    // 尝试超出边界撤销
    saveMgr.undo(hero);
    saveMgr.undo(hero);

    return 0;
}

4.2 运行输出

复制代码
===== 初始状态 =====
【角色状态】生命值:100  攻击力:50  坐标:(0, 0)

>>> 状态已存档
角色受到 30 点伤害
【角色状态】生命值:70  攻击力:50  坐标:(0, 0)

>>> 状态已存档
角色移动到坐标 (10.5, 20.8)
【角色状态】生命值:70  攻击力:50  坐标:(10.5, 20.8)

>>> 状态已存档
===== 执行第一次撤销 =====
>>> 撤销成功
【角色状态】生命值:70  攻击力:50  坐标:(0, 0)

===== 执行第二次撤销 =====
>>> 撤销成功
【角色状态】生命值:100  攻击力:50  坐标:(0, 0)

===== 执行重做 =====
>>> 重做成功
【角色状态】生命值:70  攻击力:50  坐标:(0, 0)

>>> 无法撤销,已到最早记录
>>> 无法撤销,已到最早记录

五、适用场景与禁用场景

5.1 适用场景

  1. 需要状态回滚
    编辑器、绘图软件、APP 操作记录,实现撤销/重做功能。
  2. 数据存档与恢复
    游戏存档、配置快照、用户表单临时保存、系统运行快照。
  3. 事务回滚
    业务操作异常时,恢复到操作前状态。
  4. 需要保护封装
    需保存对象内部状态,但不允许外部直接访问私有成员。

5.2 不适用场景

  1. 对象状态极其庞大,频繁快照会造成严重内存占用;
  2. 状态简单、无需历史记录,引入模式属于过度设计
  3. 状态频繁变更且无回滚需求。

六、模式优缺点

6.1 优点

  1. 封装性极佳
    对象内部状态完全对外隐藏,仅自身可读写,数据安全。
  2. 职责划分清晰
    原发器处理业务,备忘录存储数据,负责人管理历史记录,各司其职。
  3. 灵活实现多级回滚
    可自由保存多份历史快照,支持无限级撤销、重做。
  4. 状态恢复可靠
    由原发器自主完成恢复逻辑,避免外部篡改导致异常。

6.2 缺点

  1. 内存开销大
    每一次存档都会生成新备忘录对象,状态复杂时内存压力显著。
  2. 拷贝成本高
    包含容器、指针、复杂嵌套对象时,深拷贝会消耗 CPU 性能。
  3. 历史记录难以清理
    不限制存档数量会导致内存持续上涨。
  4. 类数量增多
    新增备忘录、负责人类,小幅提升代码复杂度。

七、C++ 工程实践规范与优化方案

7.1 编码规范

  1. 必加虚析构
    若存在继承关系,基类添加虚析构函数,防止智能指针析构泄漏。
  2. 严格控制访问权限
    备忘录构造、成员变量全部设为 private,仅通过友元开放权限。
  3. 禁用拷贝与赋值
    备忘录默认禁止拷贝,防止状态被意外复制篡改。
  4. 优先使用智能指针
    全程使用 std::unique_ptr 管理备忘录,减少裸指针。

7.2 性能优化方案

  1. 限制最大存档数
    负责人设置上限,自动淘汰最早记录,控制内存大小。
  2. 增量快照(进阶)
    仅保存变更部分状态,而非全量拷贝,降低拷贝开销。
  3. 延迟销毁
    低频操作场景可缓存常用备忘录,减少重复创建。
  4. 多线程防护
    多线程环境下,对存档列表、状态读写加互斥锁,保证线程安全。

7.3 复杂状态处理

  • 状态含指针/动态容器:必须执行深拷贝
  • 状态为只读配置:可复用备忘录对象,不必每次新建。

八、相关模式对比

8.1 备忘录模式 VS 命令模式

对比项 备忘录模式 命令模式
核心目标 保存对象状态快照,按状态回滚 封装操作行为,反向执行操作实现撤销
存储内容 数据状态 操作指令、执行者
撤销逻辑 直接还原历史状态 执行反向操作
适用场景 整体状态回滚(存档、编辑器) 动作序列、任务队列、批量操作

8.2 备忘录模式 VS 原型模式

  • 原型:侧重对象快速克隆,用于对象创建;
  • 备忘录:侧重状态快照与回滚,用于历史记录,二者用途完全不同。

九、总结

备忘录模式是状态快照与回滚 的专用设计模式,核心亮点是在不破坏封装的前提下实现状态保存与恢复

  1. 核心结构:原发器 + 备忘录 + 负责人 三者配合;
  2. C++ 实现关键:友元、私有构造、禁用拷贝、智能指针、深拷贝;
  3. 使用建议:适合撤销、存档、回滚类场景,务必控制快照数量,规避内存与性能问题;
  4. 选型提醒:简单状态回滚优先使用本模式,复杂动作撤销可结合命令模式。
相关推荐
Solis程序员1 小时前
MCP (Model Context Protocol):AI应用连接外部世界的标准协议
人工智能·microsoft·agent·skill·mcp
天恩软件1 小时前
一分钟学会 C++ 标准模板库智能指针
c++·智能指针
诺未科技_NovaTech2 小时前
上海诺未携手惠灵顿中国,基于微软 Azure 打造 AI 教育生态标杆
人工智能·microsoft·azure·ai教育
j7~2 小时前
【C++】STL--Vector容器--拆析解剖Vector的实现以及Vector的底层详解(1)
开发语言·c++·vector·迭代器失效·迭代器的使用
森G2 小时前
76、仿ASIO实现的Linux c++服务器------服务器源码解析----云视频服务项目
c++·qt
TCW11212 小时前
AI底层系列:用C++实现线性代数的公式推导与算法设计-6.线性方程组的解集
c++·人工智能·算法
小欣加油2 小时前
leetcode3612 用特殊操作处理字符串I
数据结构·c++·算法·leetcode·职场和发展
拳里剑气2 小时前
C++算法:链表
c++·算法·链表
superkcl20222 小时前
【QT Thread】
c++·qt