C++备忘录模式的实现方案与数据恢复机制详解插图

C++备忘录模式的实现方案与数据恢复机制详解

你好,我是源码库的博主。今天我们来深入聊聊设计模式中一个非常实用,但在实际项目中又常常被忽视的模式——备忘录模式(Memento Pattern)。记得我第一次在项目中需要实现“撤销/重做”功能时,第一反应就是疯狂写状态判断和临时变量,结果代码臃肿不堪,维护起来简直是噩梦。直到系统学习了备忘录模式,才恍然大悟:原来优雅的数据快照与恢复机制就该这么设计。这篇文章,我将结合自己的实战经验(包括踩过的坑),带你彻底掌握C++中备忘录模式的几种实现方案。

备忘录模式的核心思想与适用场景

备忘录模式,顾名思义,就是为一个对象创建一个“备忘录”,用来记录它在某个时刻的内部状态。这样,在需要的时候,这个对象可以恢复到备忘录所记录的状态。它的核心角色有三个:

  1. 原发器(Originator):需要保存和恢复状态的对象。
  2. 备忘录(Memento):负责存储原发器的内部状态。通常,原发器决定哪些状态需要存储。
  3. 管理者(Caretaker):负责保存和管理备忘录,但不能对备忘录的内容进行操作或检查。

它最适合什么场景? 除了最经典的“撤销/重做”(Undo/Redo),我还成功将它应用在游戏存档/读档、事务回滚、以及配置文件的临时修改与还原等场景中。它的最大好处是将状态保存与恢复的职责与原发器业务逻辑解耦,使得状态管理变得清晰独立。

方案一:经典实现——严格的封装与友元类

这是教科书上最常见的实现,旨在严格保护备忘录内部数据。其关键在于让Memento成为Originator的友元,这样Originator可以自由访问Memento的私有成员来保存和恢复状态,而外部管理者Caretaker只能持有备忘录对象,却看不到其内部数据。

下面是一个模拟编辑器文本状态的例子:

#include 
#include 
#include 
#include 

// 前置声明
class Memento;

// 原发器:编辑器
class Editor {
public:
    Editor(const std::string& text) : text_(text), cursorPos_(0) {}

    void type(const std::string& words) {
        text_.insert(cursorPos_, words);
        cursorPos_ += words.length();
        std::cout << "当前文本: " << text_ << ", 光标位置: " << cursorPos_ << std::endl;
    }

    void setCursor(int pos) {
        cursorPos_ = pos;
        std::cout << "移动光标到: " << cursorPos_ << std::endl;
    }

    // 创建备忘录:保存当前状态
    std::unique_ptr createMemento() const;

    // 恢复状态:从备忘录还原
    void restoreFromMemento(const Memento* memento);

    void display() const {
        std::cout << "[状态] 文本: "" << text_ << "", 光标: " << cursorPos_ << std::endl;
    }

private:
    std::string text_;
    int cursorPos_;
};

// 备忘录
class Memento {
private:
    // 关键点:构造函数和成员都是私有的
    Memento(const std::string& text, int cursorPos)
        : savedText_(text), savedCursorPos_(cursorPos) {}

    // 关键点:将原发器声明为友元
    friend class Editor;

    const std::string& getSavedText() const { return savedText_; }
    int getSavedCursorPos() const { return savedCursorPos_; }

    std::string savedText_;
    int savedCursorPos_;
};

// 在原发器类外实现其成员函数(因为它们需要访问Memento)
std::unique_ptr Editor::createMemento() const {
    return std::make_unique(text_, cursorPos_);
}

void Editor::restoreFromMemento(const Memento* memento) {
    if (memento) {
        text_ = memento->getSavedText();
        cursorPos_ = memento->getSavedCursorPos();
        std::cout << "** 状态已恢复 **" << std::endl;
    }
}

// 管理者:历史记录
class History {
public:
    void push(std::unique_ptr memento) {
        history_.push_back(std::move(memento));
    }

    std::unique_ptr pop() {
        if (history_.empty()) return nullptr;
        auto last = std::move(history_.back());
        history_.pop_back();
        return last;
    }

    bool isEmpty() const { return history_.empty(); }

private:
    std::vector<std::unique_ptr> history_;
};

int main() {
    Editor editor("Hello");
    History history;

    editor.display();
    history.push(editor.createMemento()); // 保存状态1

    editor.type(" World");
    history.push(editor.createMemento()); // 保存状态2

    editor.setCursor(5);
    editor.type(" C++");
    editor.display(); // 当前修改后的状态

    // 执行撤销(恢复到状态2)
    std::cout << "n执行撤销..." << std::endl;
    auto lastState = history.pop();
    if (lastState) {
        editor.restoreFromMemento(lastState.get());
        editor.display();
    }

    // 再次撤销(恢复到状态1)
    std::cout << "n再次撤销..." << std::endl;
    lastState = history.pop();
    if (lastState) {
        editor.restoreFromMemento(lastState.get());
        editor.display();
    }

    return 0;
}

实战提示与踩坑点:这种方案封装性最好,但引入了friend关键字,增加了耦合。在大型项目中,如果原发器状态非常复杂(比如包含大量成员变量或复杂数据结构),创建备忘录的拷贝成本可能会很高,这时需要考虑“增量备忘录”或只保存变更部分。

方案二:接口简化——将备忘录作为原发器的内部类

如果你觉得友元方案有些繁琐,并且你的项目结构允许,将Memento定义为Originator的内部类是一个更简洁的选择。这样,内部类天然拥有对外部类私有成员的访问权限,无需friend声明。

// 原发器内部定义备忘录
class Editor {
public:
    // ... 其他公共成员与方案一类似 ...

    // 内部备忘录类
    class Snapshot {
    public:
        Snapshot(const std::string& text, int pos) : text_(text), pos_(pos) {}
    private:
        friend class Editor; // 仍然需要,但范围更可控
        std::string text_;
        int pos_;
    };

    std::unique_ptr createSnapshot() const {
        return std::make_unique(text_, cursorPos_);
    }

    void restore(const Snapshot* snapshot) {
        text_ = snapshot->text_;
        cursorPos_ = snapshot->pos_;
    }

private:
    std::string text_;
    int cursorPos_;
};

// 管理者只需持有 `std::unique_ptr` 即可

这个方案代码更紧凑,关联性一目了然。但缺点是Memento类对外不完全隐藏,其类型名暴露在Originator的外部。

方案三:实战优化——序列化与持久化恢复

在真实项目中,备忘录可能不仅用于内存中的撤销栈,还需要持久化到文件或数据库,以实现关闭应用后的状态恢复(如游戏存档)。这时,我们需要让备忘录支持序列化。

一个常见的做法是让Memento提供序列化接口(如转换为std::string或JSON),并由Caretaker负责存储。恢复时,再从存储介质中读取并反序列化。

#include  // 假设使用 nlohmann/json 库
using json = nlohmann::json;

class PersistentMemento {
public:
    PersistentMemento(const json& stateJson) : stateJson_(stateJson) {}

    // 获取序列化后的状态表示
    const json& getState() const { return stateJson_; }

    // 静态方法,用于从状态创建备忘录(工厂方法)
    static std::unique_ptr createFromState(const std::string& text, int pos) {
        json j;
        j["text"] = text;
        j["cursor_position"] = pos;
        return std::make_unique(j);
    }

private:
    json stateJson_;
};

// 修改后的原发器需要能根据json恢复状态
class PersistentEditor {
public:
    // ... 其他方法 ...
    std::unique_ptr save() const {
        return PersistentMemento::createFromState(text_, cursorPos_);
    }

    void load(const PersistentMemento& memento) {
        const json& j = memento.getState();
        text_ = j["text"].get();
        cursorPos_ = j["cursor_position"].get();
        std::cout << "从持久化存储加载状态成功!" << std::endl;
    }
};

// 管理者可以这样保存到文件
// void Caretaker::saveToFile(const std::string& filename, const PersistentMemento& memento) {
//     std::ofstream file(filename);
//     file << memento.getState().dump(4); // 漂亮打印JSON
// }

重要经验:采用序列化方案时,务必注意版本兼容性。当你的Originator类新增了状态字段后,旧的备忘录文件可能无法正确解析。我踩过的坑是:没有在备忘录数据中加入版本号字段,导致游戏更新后老存档全部报废。解决方案是在序列化数据中包含一个版本标识,并在恢复逻辑中做兼容处理。

总结与选择建议

备忘录模式在C++中的实现,核心在于平衡封装性性能易用性

  • 追求严格封装:选择方案一(友元类),适合框架或库的开发。
  • 追求代码简洁与高内聚:选择方案二(内部类),适合中等规模、结构清晰的项目。
  • 需要跨会话持久化:选择方案三(序列化),并务必处理好版本控制。

最后,别忘了备忘录模式的一个潜在缺点:如果原发器状态很大且频繁保存,会消耗大量内存。在实际开发中,我通常会设置一个历史栈的深度限制,或者像一些图形软件那样,采用“命令模式+增量备忘录”的组合来实现更高效的撤销/重做栈。

希望这篇结合实战的详解能帮助你下次在C++项目中,游刃有余地实现优雅的状态恢复机制。如果有任何问题或更好的实践,欢迎在源码库交流讨论!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。