精读 C++20 设计模式:行为型设计模式 — 备忘录模式

精读 C++20 设计模式:行为型设计模式 --- 备忘录模式

前言

​ 我们现在往往会使用撤销 / 回退功能。这就意味着,咱们需要准备备忘所有的操作和他们的正反双方操作。这个在咱们的命令模式中的redo/undo模式看到了。当我们实现"撤销 / 回退"功能、快照保存、或者需要在不暴露内部实现的情况下记录对象历史状态时,备忘录模式是一个自然且常用的解决方式。它把对象状态的保存与恢复职责分离出来(备忘录),由另一个管理者(caretaker)负责保存这些备忘录,而不让外界直接访问对象内部细节。结果是既能保留封装性,又能实现状态回退、重做、历史回放等功能。


什么是备忘录模式(简短定义)

备忘录模式(Memento)允许在不暴露对象实现细节的前提下,捕获一个对象的内部状态并在以后将其恢复。角色通常包括:

  • Originator(发起者):有需保存/恢复状态的对象(例如文本编辑器)。
  • Memento(备忘录):用于保存发起者内部状态的不可变对象(或只对发起者可见)。
  • Caretaker(管理者):保存备忘录的集合并在需要时请求 Originator 恢复。

关键点:尽量保持备忘录对外界的不可变性 / 不可读性,避免外部直接操作对象内部数据。


示例 1 --- 基本 Demo(文本编辑器快照)

​ 来一个最经典的例子:

cpp 复制代码
// memento_basic.cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>

// 备忘录(简单实现,公开字段但通常应限定可见性)
struct EditorMemento {
    std::string content;
    size_t cursor_pos;
    EditorMemento(std::string c, size_t p) : content(std::move(c)), cursor_pos(p) {}
};

// 发起者:文本编辑器
class TextEditor {
public:
    void insert(const std::string& s) {
        content_.insert(cursor_pos_, s);
        cursor_pos_ += s.size();
    }

    void set_content(const std::string& s) {
        content_ = s; cursor_pos_ = s.size();
    }

    void move_cursor(size_t pos) {
        cursor_pos_ = std::min(pos, content_.size());
    }

    std::shared_ptr<EditorMemento> create_memento() const {
        return std::make_shared<EditorMemento>(content_, cursor_pos_);
    }

    void restore(std::shared_ptr<const EditorMemento> m) {
        if (!m) return;
        content_ = m->content;
        cursor_pos_ = m->cursor_pos;
    }

    void print() const {
        std::cout << "Content: \"" << content_ << "\" | Cursor@" << cursor_pos_ << "\n";
    }

private:
    std::string content_;
    size_t cursor_pos_ = 0;
};

// 简单演示
int main_basic() {
    TextEditor editor;
    editor.insert("Hello");
    editor.print();

    auto snap1 = editor.create_memento(); // 保存快照1

    editor.insert(", world!");
    editor.print();

    editor.restore(snap1); // 恢复到快照1
    editor.print();
    return 0;
}

​ 看到EditorMemento 的威力了嘛?EditorMemento每一次创建都会压入目标备份对象的状态(关于状态有状态机模式,别急)。我们完全托管了TextEditor的状态保存,对外只返回一个备忘录对象!回复的时候只需要指定回退到哪个备忘录就结束了!

增强:撤销(Undo)与重做(Redo)

单个快照适合"回到某一时刻"。若要支持连续的 撤销/重做,需要在 Caretaker 中保存一个历史序列并维护当前索引。下面给出带 undo/redo 功能的管理器实现。

cpp 复制代码
// memento_undo_redo.cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>

struct EditorMemento {
    std::string content;
    size_t cursor_pos;
    EditorMemento(std::string c, size_t p) : content(std::move(c)), cursor_pos(p) {}
};

class TextEditor {
public:
    void insert(const std::string& s) {
        content_.insert(cursor_pos_, s);
        cursor_pos_ += s.size();
    }
    void remove_back(size_t n) {
        if (n > content_.size()) n = content_.size();
        if (n > cursor_pos_) n = cursor_pos_;
        content_.erase(cursor_pos_ - n, n);
        cursor_pos_ -= n;
    }
    void move_cursor(size_t pos) { cursor_pos_ = std::min(pos, content_.size()); }

    std::shared_ptr<EditorMemento> create_memento() const {
        return std::make_shared<EditorMemento>(content_, cursor_pos_);
    }
    void restore(std::shared_ptr<const EditorMemento> m) {
        if (!m) return;
        content_ = m->content;
        cursor_pos_ = m->cursor_pos;
    }
    void print() const {
        std::cout << "Content: \"" << content_ << "\" | Cursor@" << cursor_pos_ << "\n";
    }
private:
    std::string content_;
    size_t cursor_pos_ = 0;
};

// Caretaker:管理历史,提供 undo/redo
class History {
public:
    void push(std::shared_ptr<EditorMemento> m) {
        // 如果当前不在末尾,丢弃 forward redo 历史
        if (current_index_ + 1 < static_cast<int>(history_.size())) {
            history_.erase(history_.begin() + current_index_ + 1, history_.end());
        }
        history_.push_back(std::move(m));
        current_index_ = static_cast<int>(history_.size()) - 1;
    }

    bool can_undo() const { return current_index_ > 0; }
    bool can_redo() const { return current_index_ + 1 < static_cast<int>(history_.size()); }

    std::shared_ptr<EditorMemento> undo() {
        if (!can_undo()) return nullptr;
        --current_index_;
        return history_[current_index_];
    }

    std::shared_ptr<EditorMemento> redo() {
        if (!can_redo()) return nullptr;
        ++current_index_;
        return history_[current_index_];
    }

    std::shared_ptr<EditorMemento> current() const {
        if (current_index_ < 0) return nullptr;
        return history_[current_index_];
    }

private:
    std::vector<std::shared_ptr<EditorMemento>> history_;
    int current_index_ = -1;
};

// 演示
int main_undo_redo() {
    TextEditor editor;
    History history;

    // 初始状态保存
    history.push(editor.create_memento());

    editor.insert("Hello");
    history.push(editor.create_memento());

    editor.insert(", world");
    history.push(editor.create_memento());

    editor.print(); // Hello, world

    // Undo
    if (history.can_undo()) {
        editor.restore(history.undo());
        std::cout << "[undo]\n";
        editor.print();
    }

    // Undo 再次
    if (history.can_undo()) {
        editor.restore(history.undo());
        std::cout << "[undo]\n";
        editor.print();
    }

    // Redo
    if (history.can_redo()) {
        editor.restore(history.redo());
        std::cout << "[redo]\n";
        editor.print();
    }

    // 修改新内容(会丢弃 redo 历史)
    editor.insert("!!!");
    history.push(editor.create_memento());
    editor.print();

    // 现在 redo 不可用
    std::cout << "can_redo: " << history.can_redo() << "\n";
    return 0;
}

​ 现在我们将撤销重做行为专门的抽象出History类来,每次在做用户可撤销的操作后,调用 history.push(editor.create_memento()) 保存快照。当执行 undo() 时,Caretaker 返回历史中的一个快照;发起者 restore() 即可回退。当然,当在非末尾位置执行新操作时,必须丢弃 forward redo 历史(这是符合常见编辑器行为)。

总结

我们试图解决什么问题?
  • 需要保存对象历史状态并在以后恢复(撤销、重做、历史回放)。
  • 要在不暴露对象内部实现(字段、细节)的情况下传递对象状态。
  • 希望分离"保存/恢复"逻辑与业务逻辑,使业务类保持聚焦且易测。
我们如何解决问题?
  • 发起者(Originator) 提供创建与恢复备忘录的方法(create_memento() / restore(memento))。备忘录封装状态细节。
  • 管理者(Caretaker) 保存备忘录序列,负责历史管理(push / undo / redo / 序列化保存 / 传输)。
  • 在需要模块间交互时,传递只读或受控的备忘录副本,保持封装安全。
优点
  • 封装性好:不直接暴露内部实现给外部,备忘录作为状态容器即可。
  • 易实现撤销/回退:通过保存快照能方便实现 undo/redo、历史回放。
  • 模块解耦:管理历史的逻辑从业务类分离,便于测试、扩展。
  • 灵活的持久化/传输:Caretaker 可以将备忘录序列保存到磁盘或发送到远程,不改变发起者代码。
缺点与缓解
  1. 内存/存储开销 :如果状态很大且频繁保存,历史会消耗大量内存/磁盘。
    • 缓解:采取增量快照(差量存储)、压缩、限制历史深度、或者用命令模式只存反向操作(而不是完整状态)。
  2. 隐私/访问控制 :如果备忘录暴露敏感内部结构,可能泄漏信息。
    • 缓解:保证备忘录只对发起者可写、对外只提供只读视图;或使用接口隐藏内部字段。
  3. 复杂性转移到 Caretaker :管理撤销/重做、合并、分支历史会让 Caretaker 变复杂。
    • 缓解:把策略拆分成子模块(如历史存储、合并策略、垃圾回收),并为 Caretaker 做良好单元测试。
  4. 与并发/分布式场景的协调 :在多线程或分布式环境,备忘录的一致性、合并冲突需要额外处理。
    • 缓解:采用版本号、冲突检测/合并策略或使用 Operational Transformation / CRDT 等更适合协作编辑的方案。
相关推荐
青草地溪水旁2 小时前
设计模式(C++)详解——观察者模式(Observer)(2)
c++·观察者模式·设计模式
我的xiaodoujiao2 小时前
Web UI自动化测试学习系列5--基础知识1--常用元素定位1
windows·python·学习·测试工具
张永清-老清2 小时前
每周读书与学习->初识JMeter 元件(三)
学习·测试工具·性能调优·jmeter性能测试·每周读书与学习
陈鹏鹏勇闯天涯3 小时前
C++智能指针
c++
希望_睿智3 小时前
实战设计模式之迭代器模式
c++·设计模式·架构
charlie1145141913 小时前
精读C++20设计模式——行为型设计模式:策略模式
c++·学习·设计模式·策略模式·c++20
郝学胜-神的一滴3 小时前
深入理解 Qt 元对象系统:QMetaEnum 的应用与实践
开发语言·c++·qt·软件工程
Brookty3 小时前
【Java学习】定时器Timer(源码详解)
java·开发语言·学习·多线程·javaee
艾莉丝努力练剑3 小时前
【C++STL :vector类 (二) 】攻克 C++ Vector 的迭代器失效陷阱:从源码层面详解原理与解决方案
linux·开发语言·c++·经验分享