1. 备忘录模式(Memento Pattern)的定义
(1)定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
①不破坏封装性:对象不能暴露它不应该暴露的细节。
②捕获对象的内部状态:保存状态的目的是为了恢复,把以将某个对象的状态保存在备忘录对象中,以便以后的恢复。
③对象的内部状态保存在备忘录对象中,而备忘录对象通常被存储在原发器对象之外,一般保存在管理者对象那里。
(2)备忘录模式的结构和说明
①Originator:原发器。使用备忘录来保存某个时刻原发器自身的状态,也可以使用备忘录来恢复这些内部状态。
②备忘录:主要用来存储原发器对象的内部状态,但是具体需要存储哪些数据是由原发器对象来决定的。另外备忘录应该只能由原发器对象来访问它的内部数据,原发器外部的对象不应该访问到备忘录对象的内部数据。
③Caretaker:备忘录的管理者。主要负责保存备忘录对象,但是不能对备忘录对象的内部进行操作或检查。
(3)备忘录模式的本质:保存和恢复内部状态
2. 深入理解备忘录模式
(1)备忘录对象
①通常就是用来记录原发器对象需要保存的状态信息。
②备忘录对象是用来封装数据的,但与普通的封装数据的对象不同,备忘录对象一般只让原发器对象操作。为了保存这一点,通常会把备忘录对象作为原发器对象的内部类来实现,而且实现成私有的,然后通常一个窄接口来标识对象的类型,以便以外部交互。
(2)原发器对象
①需要被保存状态的对象,该对象应该提供捕获某个时刻对象内部状态的方法,在这个方法中,原发器对象会创建备忘录对象,把需要保存的状态数据设置到备忘录对象中,然后把备忘录对象提供给管理者对象来保存。
②当然,原发器对象也提供了利用备忘录对象来恢复内部状态的方法。
(3)管理者对象:主要负责保存备忘录对象
①并不一定需要一个管理者对象。广义来说,调用原发器获得备忘录对象后,备忘录放在哪里,那个对象就是管理者对象。
②管理者对象并不是只管理一个备忘录对象,它可以管理多个备忘录对象。
③狭义的管理者只管理同一类的备忘录对象,但广义的管理者可以管理不同类型的备忘录对象。
④管理者对象需要实现的基本功能是:存入备忘录对象和从中获取备忘录对象。从功能上看,就是一个缓存功能或一个简单的对象实例池。
⑤管理者虽然能存取备忘录对象,但是不能访问备忘录对象的内部数据。
(4)宽接口和窄接口
①在实现Memento模式中,要防止原发器以外的对象访问备记录对象,备忘录对象有两个接口,一个为原发器使用的宽接口,一个为其他对象使用的窄接口。
②窄接口:管理者只能看到备忘录的窄接口,这个接口的实现通常没有任何的方法,只是一个类型标识。窄接口使得管理者只能将备忘录传递给其他对象
③宽接口:原发器能够看到一个宽接口,允许它访问所需的所有数据,来返回先前的状态。通常实现成为原发器内的一个私有内部类。
(5)使用备忘录模式的潜在代价
在实现Memento模式是,要考虑拷贝对象状态的效率问题,如果对象开销比较大,可以采用某个增量式改变来改进Memento模式。
(6)增量存储
如果需要频繁地创建备忘录对象,而且创建和应用备忘录对象来恢复状态的顺序是可控的,那就可以使用增量存储,也就是备忘录仅仅存储原发器内部相对于上一次存储状态后的增量改变。
【编程实验】游戏进度的保存
//行为型模式——备忘录模式 //场景:游戏进度的备份 #include <iostream> using namespace std; //Memento抽象类 class Memento { }; //Originator:游戏角色 class GameRole { private: int vitality; //生命力 int attack; //攻击力 int defense; //防御力 //内部类,用于保存和恢复游戏进度 //备忘录对象类 class RoleStateMementoImpl : public Memento { private: int vit; int atk; int def; public: RoleStateMementoImpl(GameRole* role) { vit = role->getVitality(); atk = role->getAttack(); def = role->getDefense(); } int getVitality(){return vit;} int getAttack(){return atk;} int getDefense(){return def;} }; public: int getVitality() { return vitality; } void setVitality(int value) { vitality = value; } int getAttack() { return attack; } void setAttack(int value) { attack = value; } int getDefense() { return defense; } void setDefense(int value) { defense = value; } //状态显示 void display() { cout << "角色当前状态:" << endl; cout << "生命力:" << vitality << endl; cout << "攻击力:" << attack << endl; cout << "防御力:" << defense << endl; } //获取初始状态(数据通常来自本机或远程数据库) void initState() { vitality = 100; attack = 100; defense = 100; } //战斗(与Boss大战后,游戏数据的损耗) void fight() { vitality = 40; attack = 20; defense = 30; } //保存备份 Memento* saveState() { return new RoleStateMementoImpl(this); } //恢复备份 void recoveryState(Memento* memento) { RoleStateMementoImpl* mmo = reinterpret_cast<RoleStateMementoImpl*>(memento); if (mmo != NULL) { vitality = mmo->getVitality(); attack = mmo->getAttack(); defense = mmo->getDefense(); } } }; //备忘录管理者 class RoleStateCaretaker { private: Memento* mmo; public: Memento* getMemento() { return mmo; } void setMemento(Memento* value) { mmo = value; } }; int main() { //大战Boss前 cout << "大战Boss前:" << endl; GameRole* role = new GameRole(); role->initState(); role->display(); //保存进度 RoleStateCaretaker stateAdmin; stateAdmin.setMemento(role->saveState()); //大战Boss后,损耗严重 cout << endl << "大战Boss后:" << endl; role->fight(); role->display(); //恢复之前的状态 cout << endl << "恢复到大战Boss前的状态:" << endl; role->recoveryState(stateAdmin.getMemento()); role->display(); return 0; } /*输出结果 大战Boss前: 角色当前状态: 生命力:100 攻击力:100 防御力:100 大战Boss后: 角色当前状态: 生命力:40 攻击力:20 防御力:30 恢复到大战Boss前的状态: 角色当前状态: 生命力:100 攻击力:100 防御力:100 */
3. 备忘录模式的应用场景
(1)撤销和回滚:如悔棋,数据库中的事务回滚,Photoshop中的历史记录等。
(2)如果需要保存一个对象某一个时刻或部分状态,方便以后恢复的。
(3)如果需要保存一个对象的内部状态,又不暴露该对象的实现细节。
4. 备忘录模式的几种典型用法
(1)结合原型模式
①在原发器对象创建备忘录对象里,如果原发器对象中全部或大部分的状态都需要保存,就可以直接克隆一个。即原发器对象实现可克隆的功能。
②备忘录对象只需要保存克隆出来的对象实例就可以了
③相应创建和设置备忘录对象的地方都要修改。
(2)离线存储
①可以将备忘录的数据实现为离线存储在文件中、XML或数据库中,从而支持跨越会话的备份和恢复功能。
②在需要的时候从离线存储中获取相应的数据,然后重新设置状态,恢复到之前的备份。
(3)实现可撤销的操作
①命令模式中,可撤销的操作的实现有两种基本思想,一种是补偿式(反操作式),如加变减操作。打开变关闭的操作。
②另一种方式是存储恢复式,即把操作前的状态记录下来,然后要撤销操作时,就可以直接恢复回去。
【编程实验】可撤销操作的计算器(命令模式+备忘录模式)
//行为型模式——备忘录模式 //场景:可撤销操作的计算器(命令模式+备忘录模式) #include <iostream> #include <list> using namespace std; //Memento抽象类 class Memento { //空的 }; //定义命令接口 class Command { public: //执行命令 virtual void execute() = 0; //撤销命令,恢复到备忘录对象记录的状态 virtual void undo(Memento* m) = 0; //重做命令,恢复到备忘录对象记录的状态 virtual void redo(Memento* m) = 0; //创建备忘录对象 virtual Memento* createMemento() = 0; }; //操作运算的接口 class OperationApi { public: virtual int getResult() = 0; virtual void add(int num) = 0; virtual void substract(int num) = 0; //创建保存原发器对象状态的备忘录对象 virtual Memento* createMemento() = 0; //重新设置原发器对象状态,让其回到备忘录对象 //记录的状态。 virtual void setMemento(Memento* m) = 0; }; //命令对象的公共对象,实现各个命令对象的公共方法 class AbstractCommand : public Command { protected: //持有真正的命令实现者对象 OperationApi* operation; public: virtual void execute() = 0; void setOperation(OperationApi* operation) { this->operation = operation; } void undo(Memento* m) { operation->setMemento(m); } void redo(Memento* m) { operation->setMemento(m); } Memento* createMemento() { return operation->createMemento(); } }; //加法命令 class AddCommand : public AbstractCommand { private: int opeNum; public: AddCommand(int opeNum) { this->opeNum = opeNum; } void execute() { operation->add(opeNum); } }; //减法命令 class SubstractCommand : public AbstractCommand { private: int opeNum; public: SubstractCommand(int opeNum) { this->opeNum = opeNum; } void execute() { operation->substract(opeNum); } }; //运算类(相当于原发器对象) //与原来相比,不再提供setResult方法,内部状态,不允许外部来操作 //添加了createMemento和setMemento方法的实现 //添加实现了一个私有的备忘录对象 class Operation : public OperationApi { private: int result; //计算结果 //内部类 class MementoImpl: public Memento { private: int result; public: MementoImpl(int result) { this->result = result; } int getResult() {return result;} }; public: int getResult(){return result;} void add(int num){result += num;} void substract(int num){result -= num;} Memento* createMemento() { MementoImpl* m = new MementoImpl(result); return m; } void setMemento(Memento* m) { MementoImpl* impl = (MementoImpl*)m; result = impl->getResult(); } }; //计算器类(有加法按钮、减法按钮,还有撤销和恢复按钮) class Calculator { private: //命令操作的历史记录,在撤销时用 list<Command*> undoCmds; //命令被撤销的历史记录,在恢复时用 list<Command*> redoCmds; //每个命令都有执行前和执行后的两个状态 //与之对应,即每执行一个命令,会首先压入执行前的 //状态,再压入执行后的状态 list<Memento*> undoMementos; //用于撤销时用 list<Memento*> redoMementos; //用于恢复时用 Command* addCmd; Command* subCmd; public: void setAddCmd(Command* addCmd) { this->addCmd = addCmd; } void setSubstractCmd(Command* subCmd) { this->subCmd = subCmd; } //按下加法按钮 void addPressed() { //执行命令前先备份一下。 Memento* m1 = addCmd->createMemento(); //执行命令 addCmd->execute(); //把操作记录到历史记录中 undoCmds.push_back(addCmd); //执行命令后再备份一下 Memento* m2 = addCmd->createMemento(); //设置到撤销的历史记录中(分别压入命令执行前、后两个状态) undoMementos.push_back(m1); undoMementos.push_back(m2); } //减法按钮 void substractPressed() { //执行命令前先备份一下。 Memento* m1 = subCmd->createMemento(); //执行命令 subCmd->execute(); //把操作记录到历史记录中 undoCmds.push_back(subCmd); //执行命令后再备份一下 Memento* m2 = subCmd->createMemento(); //设置到撤销的历史记录中(分别压入命令执行前、后两个状态) undoMementos.push_back(m1); undoMementos.push_back(m2); } //撤销操作 void undoPressed() { if(undoCmds.size() > 0) { //取出最后一个命令来撤销 Command* cmd = undoCmds.back(); //获取对应的备忘录对象 Memento* m2 = undoMementos.back(); undoMementos.pop_back(); Memento* m1 = undoMementos.back(); undoMementos.pop_back(); //撤销(当然是回到该按钮执行前的状态) cmd->undo(m1); //加入恢复功能 redoCmds.push_back(cmd); //把相应的备记录对象也添加过去 redoMementos.push_back(m1); redoMementos.push_back(m2); //从撤销中删除最后一个己执行后的命令) undoCmds.pop_back(); } else { cout <<"很抱歉,没有可撤销的命令!" << endl; } } //恢复操作 void redoPressed() { if(redoCmds.size() > 0) { //取出最后一个命令来重做 Command* cmd = redoCmds.back(); //获取对应的备忘录对象 Memento* m2 = redoMementos.back(); redoMementos.pop_back(); Memento* m1 = redoMementos.back(); redoMementos.pop_back(); //重做(当然是回到该按钮执行后的状态) cmd->redo(m2); //加入撤销队列中 undoCmds.push_back(cmd); //把相应的备记录对象也添加过去 undoMementos.push_back(m1); undoMementos.push_back(m2); //从重做队列中删除最后一个己执行的命令) redoCmds.pop_back(); } else { cout <<"很抱歉,没有可恢复的命令!" << endl; } } }; int main() { //1、组装命令和接收者 //创建接收者 OperationApi* operation = new Operation(); //创建命令 AddCommand addCmd(5); SubstractCommand subCmd(3); //组装命令和接收者 addCmd.setOperation(operation); subCmd.setOperation(operation); //2.把命令设置到持有者,就是计算器中 Calculator& cal = *(new Calculator()); cal.setAddCmd(&addCmd); cal.setSubstractCmd(&subCmd); //3.模拟按下按钮,测试一下 cal.addPressed(); cout << "一次加法运算后的结果:" << operation->getResult() << endl; cal.substractPressed(); cout << "一次减法运算后的结果:" << operation->getResult() << endl; //测试撤销 cal.undoPressed(); cout << "撤销一次后的结果:" << operation->getResult() << endl; cal.undoPressed(); cout << "再撤销一次后的结果:" << operation->getResult() << endl; //测试恢复 cal.redoPressed(); cout << "恢复一次后的结果:" << operation->getResult() << endl; cal.redoPressed(); cout << "再恢复一次后的结果:" << operation->getResult() << endl; return 0; } /*输出结果 一次加法运算后的结果:5 一次减法运算后的结果:2 撤销一次后的结果:5 再撤销一次后的结果:0 恢复一次后的结果:5 再恢复一次后的结果:2 */
5. 备忘录模式的优缺点
(1)优点
①更好的封装性,通过使用备忘录对象来封装原发器对象的内部状态,虽然这个对象保存在原发器对象的外部,但是由于备忘录对象的窄接口并不提供任何方法,因为有效地保证了原发器内部状态的封装,不把原发器对象的内部实现细节暴露给外部。
②简化了原发器,备忘录对象被保存在原发器对象之外,让客户来管理他们请求的状态,从而让原发器对象得到简化。
(2)缺点:频繁地创建备忘录对象,可能导致较大的开销
6. 相关模式
(1)备忘录模式和命令模式
命令模式实现中,在实现命令的撤销和重做的时候,可以使用备忘录模式,在命令操作的时候记录下操作前后的状态,然后在命令撤销和重做的时候,直接使用相应的备忘录对象来恢复状态就可以了。
(2)备忘录模式和原型模式
创建备忘录对象时,如果原发器对象中全部或大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例。