21. 备忘录模式:模式动机与定义、角色与结构、C++代码实现、游戏存档示例
模式动机与定义
你有没有遇到过这种情况?
辛辛苦苦写了一大段代码,或者游戏里打了半天Boss,突然一个误操作——全没了。想回退?没门。
我早年做编辑器的时候,就吃过这个亏。用户拖拽一个控件,不小心放错了位置,想撤销,结果发现撤销栈只记录了操作名,没保存状态。用户当场炸毛。嗯,从那以后,我对“状态恢复”这件事就特别敏感。
备忘录模式,说白了就是:在不破坏封装性的前提下,捕获并外部化一个对象的内部状态,以便以后可以将该对象恢复到原先保存的状态。
你想想看,它解决的核心问题是什么?
- 需要保存某个时刻的快照
- 但又不想暴露内部细节(比如私有成员变量)
- 希望恢复操作简单直接
我个人的习惯是:只要一个对象的状态需要“回滚”或“撤销”,我就会考虑备忘录模式。它不像命令模式那么重,但胜在干净利落。
角色与结构
备忘录模式有三个核心角色,我画了张图帮你理清关系:
角色说明:
- Originator(发起人):需要保存状态的对象。它负责创建备忘录,也能从备忘录恢复状态。
- Memento(备忘录):存储 Originator 内部状态的对象。对外部只读,只有 Originator 能读写。
- Caretaker(管理者):负责保存备忘录,但不能修改备忘录内容。它只负责存取。
核心要点:备忘录模式的关键在于“封装”。Caretaker 拿到的备忘录,只能看不能改。Originator 内部的状态细节,对外界完全不可见。
C++代码实现
直接上代码。我写了一个简单的文本编辑器状态保存示例,你一看就明白。
// 备忘录:存储文本状态
class Memento {
private:
std::string state_;
// 只有 Originator 能访问私有成员
friend class TextEditor;
Memento(const std::string& s) : state_(s) {}
void setState(const std::string& s) { state_ = s; }
std::string getState() const { return state_; }
};
// 发起人:文本编辑器
class TextEditor {
private:
std::string text_;
public:
void write(const std::string& words) {
text_ += words;
}
std::string getText() const {
return text_;
}
// 创建快照
std::unique_ptr<Memento> save() {
return std::make_unique<Memento>(text_);
}
// 从快照恢复
void restore(const Memento& memento) {
text_ = memento.getState();
}
};
// 管理者:历史记录
class History {
private:
std::vector<std::unique_ptr<Memento>> snapshots_;
public:
void push(std::unique_ptr<Memento> m) {
snapshots_.push_back(std::move(m));
}
std::unique_ptr<Memento> pop() {
if (snapshots_.empty()) return nullptr;
auto m = std::move(snapshots_.back());
snapshots_.pop_back();
return m;
}
};
个人经验:这里我用了 std::unique_ptr 管理备忘录生命周期。实际项目中,如果快照很大,建议用共享指针或者对象池,避免频繁拷贝。我曾经在一个文档编辑器里用裸指针,结果内存泄漏查了一整天……
游戏存档示例
游戏存档是备忘录模式的经典应用。我们模拟一个RPG游戏角色存档:
// 游戏角色状态
struct PlayerState {
int hp;
int mp;
int level;
std::string sceneName;
};
// 存档(备忘录)
class SaveFile {
private:
PlayerState state_;
friend class GameCharacter;
SaveFile(const PlayerState& s) : state_(s) {}
PlayerState getState() const { return state_; }
};
// 游戏角色(发起人)
class GameCharacter {
private:
PlayerState state_;
public:
GameCharacter() {
state_ = {100, 50, 1, "新手村"};
}
void takeDamage(int dmg) {
state_.hp -= dmg;
if (state_.hp < 0) state_.hp = 0;
}
void levelUp() {
state_.level++;
state_.hp += 20;
state_.mp += 10;
}
void moveTo(const std::string& scene) {
state_.sceneName = scene;
}
void showStatus() const {
std::cout << "HP:" << state_.hp
<< " MP:" << state_.mp
<< " Lv:" << state_.level
<< " 场景:" << state_.sceneName << std::endl;
}
// 存档
std::unique_ptr<SaveFile> saveGame() {
return std::make_unique<SaveFile>(state_);
}
// 读档
void loadGame(const SaveFile& file) {
state_ = file.getState();
}
};
// 存档管理器(管理者)
class SaveManager {
private:
std::map<int, std::unique_ptr<SaveFile>> saves_;
int nextSlot_ = 1;
public:
int save(std::unique_ptr<SaveFile> file) {
int slot = nextSlot_++;
saves_[slot] = std::move(file);
return slot;
}
std::unique_ptr<SaveFile> load(int slot) {
if (saves_.count(slot)) {
return std::make_unique<SaveFile>(*saves_[slot]);
}
return nullptr;
}
};
使用示例:
int main() {
GameCharacter hero;
SaveManager manager;
hero.showStatus(); // 初始状态
// 打怪升级
hero.takeDamage(30);
hero.levelUp();
hero.moveTo("黑暗森林");
hero.showStatus(); // 当前状态
// 存档
int slot = manager.save(hero.saveGame());
std::cout << "存档成功,槽位:" << slot << std::endl;
// 继续冒险,角色濒死
hero.takeDamage(80);
hero.moveTo("墓地");
hero.showStatus(); // 惨状
// 读档恢复
auto file = manager.load(slot);
if (file) {
hero.loadGame(*file);
std::cout << "读档成功!" << std::endl;
hero.showStatus(); // 回到存档点
}
return 0;
}
避坑指南:我曾经在存档里直接保存了指针,结果下次启动程序时指针指向的内存早就变了。记住:备忘录保存的是值,不是引用或指针。如果需要保存复杂对象,务必实现深拷贝或者序列化。
你看,备忘录模式其实不复杂。它就是把“状态快照”这个需求,用封装的方式优雅地解决了。我个人觉得,它是设计模式里性价比很高的一个——代码量不大,但带来的灵活性非常大。
实际项目中,我经常把它和命令模式搭配使用。命令模式记录操作,备忘录模式保存状态,两者结合可以实现完美的撤销/重做系统。嗯,这个组合拳,后面有机会再细聊。