C++设计模式_行为型模式_备忘录模式Memento
本文记录备忘录模式。
备忘录(Memento)模式也称为快照(Snapshot)模式,是一种行为型模式,是为防止数据丢失而对数据进行备份以备将来恢复这些数据所采用的一种设计模式。模式可以将某个时间点的对象内部的状态保存下来,后续在必要时,根据保存的内容将该对象恢复到当时的状态。
一个具体实现范例
以单机游戏打boss为例,游戏中每个关卡的末尾都有一个BOSS(游戏中战斗力很强的怪物)等待玩家挑战,只有成功击败该BOSS,玩家才能进入下一关,游戏才能继续进行。但是BOSS是游戏中由计算机控制的具有人工智能且非常厉害的角色,在与其进行的战斗中,玩家所操控的角色非常容易被BOSS击败从而导致整个游戏结束,此时玩家必须从头开始进行游戏。这让玩家挫败感非常强,甚至有部分玩家想要放弃这款游戏。
为了解决这个问题,策划提出在快进入游戏关卡末尾之前(快遇到BOSS时),在游戏场景中摆放一个NPC(非玩家角色),玩家可以通过鼠标与该NPC进行交互,花费一定的金钱来保存此时此刻的游戏进度,如果玩家在与BOSS战斗中被击败则可以利用游戏中的“载入进度”功能将刚才保存过的游戏进度重新载人,这样玩家就可以立即再次尝试与该BOSS进行战斗而不至于从头开始进行游戏。
本节要解决的问题就是如何保存玩家的游戏进度信息的问题。这里的游戏进度信息是指玩家信息,包括玩家当前的生命值、魔法值、攻击力等信息。
class Fighter;class FighterMemento{private:FighterMemento(int attack, int life, int magic) : m_attack(attack), m_life(life), m_magic(magic){}private:// 设置友元类 friend class Fighter;// set 和 get 攻击力,生命值和魔法值void setAttack(int attack){m_attack = attack;}int getAttack(){return m_attack;}void setLife(int life){m_life = life;}int getLife(){return m_life;}void setMagic(int magic){m_magic = magic;}int getMagic(){return m_magic;}private:int m_attack;int m_life;int m_magic;};class Fighter{public:Fighter(int attack, int life, int magic) : m_attack(attack), m_life(life), m_magic(magic){}void setToDead(){m_life = 0;}void displayInfo(){// 输出角色的信息cout << "攻击力:" << m_attack;cout << "生命值:" << m_life;cout << "魔法值:" << m_magic << endl;}// 新创建一个备忘录,将玩家信息写入到备忘录中FighterMemento* saveToMemento(){return new FighterMemento(m_attack, m_life, m_magic);}// 从备忘录中恢复数据void restoreMomento(FighterMemento* pm){m_attack = pm->getAttack();m_life = pm->getLife();m_magic = pm->getMagic();}private:int m_attack;int m_life;int m_magic;};void test(){// 创建一个战斗者Fighter* pF = new Fighter(200, 200, 400);pF->displayInfo();// 备份当前的状态,pMemento 中保存了当前的状态FighterMemento* pMemento = pF->saveToMemento();// 模拟战斗,战斗失败,生命值为0pF->setToDead();pF->displayInfo();// 从备忘录中恢复数据pF->restoreMomento(pMemento);pF->displayInfo();/*攻击力:200生命值:200魔法值:400攻击力:200生命值:0魔法值:400攻击力:200生命值:200魔法值:400*/}
可以看到,上述备忘录类FighterMemento中大多数字段都是用private 进行修饰,避免其内部的数据被轻易更改,尤其是将构造函数也用private修饰,以防止该类对象被随意创建,但是将Fighter声明为了友元类,这表示Fighter类对象可以随意访问FighterMemento。有了备忘录类,就可以将玩家信息保存在备忘录对象中了。FighterMemento* saveToMemento() 函数用于将当前对象的状态保存在备忘录中,同时返回概备忘录指针,以便后期恢复。
void test() 函数中,现创建了Fighter对象,然后在Fighter对象中调用了saveToMement()保存当前对象并返回备忘录对象。当fighter需要恢复时,从备忘录中恢复数据。
此外,备忘录模式一般还会引人一个管理者(负责人)类,实现如下:
添加备忘录管理者类:
namespace _sp2
{// 引入管理者类,管理 FighterMementoclass Fighter;class FighterMemento{private:FighterMemento(int attack, int life, int magic) : m_attack(attack), m_life(life), m_magic(magic){}private:// 设置友元类 friend class Fighter;// set 和 get 攻击力,生命值和魔法值void setAttack(int attack){m_attack = attack;}int getAttack(){return m_attack;}void setLife(int life){m_life = life;}int getLife(){return m_life;}void setMagic(int magic){m_magic = magic;}int getMagic(){return m_magic;}private:int m_attack;int m_life;int m_magic;};class Fighter{public:Fighter(int attack, int life, int magic) : m_attack(attack), m_life(life), m_magic(magic){}void setToDead(){m_life = 0;}void displayInfo(){// 输出角色的信息cout << "攻击力:" << m_attack;cout << "生命值:" << m_life;cout << "魔法值:" << m_magic << endl;}// 新创建一个备忘录,将玩家信息写入到备忘录中FighterMemento* saveToMemento(){return new FighterMemento(m_attack, m_life, m_magic);}// 从备忘录中恢复数据void restoreMomento(FighterMemento* pm){m_attack = pm->getAttack();m_life = pm->getLife();m_magic = pm->getMagic();}private:int m_attack;int m_life;int m_magic;};// 管理 memetoclass TakeCareFighterMemento{public:TakeCareFighterMemento(FighterMemento* ptmp) : m_pfm(ptmp) {}void setFighterMemento(FighterMemento* ptmp){m_pfm = ptmp;}FighterMemento* getFighterMemento(){return m_pfm;}private:FighterMemento* m_pfm;};void test(){// 创建战斗者Fighter* pF = new Fighter(200, 200, 400);pF->displayInfo();// 备份自己FighterMemento* p = pF->saveToMemento(); // 创建备忘录对象,同时将自己的状态保存到备忘录对象中TakeCareFighterMemento* pT = new TakeCareFighterMemento(p); // 再将备忘录对象添加到备忘录管理者中// 和boss战斗pF->setToDead();pF->displayInfo();// 恢复之前状态pF->restoreMomento(pT->getFighterMemento());pF->displayInfo();/*攻击力:200生命值:200魔法值:400
攻击力:200生命值:0魔法值:400
攻击力:200生命值:200魔法值:400*/}
}
上面使用TakeCareFighterMemento 来管理备忘录。
还有一种实现方法,让管理者可以管理多个对象,实现如下:
管理多个对象:
namespace _sp3
{// 引入管理者类,管理 FighterMementoclass Fighter;class FighterMemento{private:FighterMemento(int attack, int life, int magic) : m_attack(attack), m_life(life), m_magic(magic){}private:// 设置友元类 friend class Fighter;// set 和 get 攻击力,生命值和魔法值void setAttack(int attack){m_attack = attack;}int getAttack(){return m_attack;}void setLife(int life){m_life = life;}int getLife(){return m_life;}void setMagic(int magic){m_magic = magic;}int getMagic(){return m_magic;}private:int m_attack;int m_life;int m_magic;};class Fighter{public:Fighter(int attack, int life, int magic) : m_attack(attack), m_life(life), m_magic(magic){}void setToDead(){m_life = 0;}void displayInfo(){// 输出角色的信息cout << "攻击力:" << m_attack;cout << "生命值:" << m_life;cout << "魔法值:" << m_magic << endl;}// 新创建一个备忘录,将玩家信息写入到备忘录中FighterMemento* saveToMemento(){return new FighterMemento(m_attack, m_life, m_magic);}// 从备忘录中恢复数据void restoreMomento(FighterMemento* pm){m_attack = pm->getAttack();m_life = pm->getLife();m_magic = pm->getMagic();}private:int m_attack;int m_life;int m_magic;};// 管理 memetoclass TakeCareMultiFighterMemento{public:~TakeCareMultiFighterMemento(){if (m_vecFm.size() > 0){for (auto iter = m_vecFm.begin(); iter != m_vecFm.end(); ++iter){delete* iter;cout << "析构执行";}}}// void setFighterMemento(FighterMemento* ptmp){m_vecFm.push_back(ptmp);}FighterMemento* getFighterMemento(int index){if (index < m_vecFm.size()){return m_vecFm[index];}return nullptr;}private://FighterMemento* m_pfm;vector<FighterMemento*> m_vecFm;};void test(){// 创建一个战斗者Fighter* pFighter = new Fighter(100, 200, 300);pFighter->displayInfo();// 创建一个备忘录管理者TakeCareMultiFighterMemento* pT = new TakeCareMultiFighterMemento();// 备份战斗者对象的状态pT->setFighterMemento(pFighter->saveToMemento());// 与boss战斗pFighter->setToDead();pFighter->displayInfo();// 再次备份pT->setFighterMemento(pFighter->saveToMemento());// 恢复第一次备份的状态pFighter->restoreMomento(pT->getFighterMemento(0));pFighter->displayInfo();delete pT;delete pFighter;}
}
引入备忘录(Memento)模式
引人“备忘录”设计模式的定义: 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。
针对前面的代码范例绘制以下备忘录模式的 UML图,如图 18.1所示。备忘录模式的UML图中包含3种角色。
(1) Originator(原发器):一个普通的类(业务类),但可以创建一个备忘录用于保存其当前的内部状态,后续也可以使用备忘录来恢复其内部的状态,将需要保存内部状态的类称原发器,这里指Fighter类。
(2) Memento(备忘录):备忘录本身是一个对象,用于存储原发器对象在某个瞬间的内部状态,备忘录的设计一般会参考原发器的设计。除原发器(创建了本备忘录对象的原发器)外,其他对象不应该直接使用(访问或修改)备忘录,或者说原发器与备忘录之间是需要进行信息共享但又不让其他类知道的,所以备忘录的接口一般都使用private修饰并将原发器类设置为友元类。备忘录是被动的,只有创建备忘录的原发器才会对它的状态进行赋值和检索,这样就避免了暴露只应该由原发器管理却又必须存储在原发器之外的信息。这里指FighterMemento类。
(3) CareTaker(负责人/管理者):负责保存好备忘录,也可以将备忘录传递给其他对象,但不需要知道备忘录对象的细节,也不能对备忘录中的内容进行操作或检查。这里指TakeCareMultiFighterMemento类。
几点说明:
(1 ) 把玩家主角类(Fighter类/原发器)中的信息保存到备忘录中,这称为给玩家主角对象做了一个快照。在本范例中,玩家主角类(Fighter类原发器)中的生命值、魔法值、攻击力这些重要的信息都有必要保存在备忘录中,所以在备忘录类(FighterMemento)中也有生命值、魔法值、攻击力字段与玩家主角类相对应,但这不意味着所有玩家主角类中的信息都要保存在备忘录中,程序员只需要依据自己的判断玩家主角类中必要的信息保存到备忘录中即可。
(2) 之所以做快照或者说做备忘录,主要目的是将来能够将数据进行恢复(复原),说得重明确一些,就是当玩家被BOSS击败的时候,玩家可以通过将备忘录类对象中的数据恢复利玩家主角类对象中来,这样玩家就可以持续不断地再次挑战BOSS直至战胜为止,可以将数据内存流、字符串或通过编码(例如Hex编码、Base64 编码)等方式存储,但是这样会涉及到数据序列化的范畴,一般比较复杂,但是执行效率更高。
(3)上面的第三个实现方式,给原发器做了多次快照,并保存在了TakeCareMultiFighterMemento对象的容器中。
(4) 虽然友元类在一定程度上破坏了封装性,但备忘录模式的主要特点是信息的隐藏,这代表两个意思,
①原发器要保持其封装性,该隐藏的内容要隐藏好;
②原发器还需要将内部状态保存到外面的备忘录中,备忘录类FighterMemento中将Figbter类声明为友元类的原因和必要性是备忘录类不应该对外暴露能更改其中数据的接口(外界不能随意修改备忘录对象)
(5) 备忘录模式更适合保存原发器对象中的一部分(不是所有)内部状态,如果需要保存原发器对象的所有内部状态,就应该采用原型模式进行原发器对象的克隆来取代备忘录模式。但从信息隐藏的角度来讲,采用备忘录模式实现也许更具优势,因为备忘录模式才可实现将对象的内部状态保存在对象之外)后续要恢复数据时也需要对象自己去读取。
(6)备忘录模式的优点是提供了一种状态恢复机制,使用户可以方便地回到一个特定的历史步骤。备忘录模式的缺点是对资源的消耗,如果原发器对象有太多的内部状态需要保存,那么每做一个快照(备忘录)都会消耗一定的内存资源。
(7) 如果将常规的做快照称为完全存储,那么为了应对需要频繁做快照的情形,可以考虑采用增量存储,也就是在备忘录中只保存原发器对象内部相对于上次存储状态之后的增量改变,例如:
记录一个图形移动时候,不用每次都记录图形的新位置,而是记录该图形向某个方
向移动的距离。完全存储可以和增量存储方式结合使用,例如,使用一次完全存储,再使用几次增量存储,然后再使用一次完全存储,再使用几次增量存储。当需要恢复数据到某个阶段的时候,可能只需要一次完全存储以及几次增量存储就可以做到。上述概念在Redis数据库的备份中也有体现,之后学习redis时,学习Redis的RDB(完全备份)和AOF(增量备份)相关知识。
(8) 备忘录模式可以应用在很多场合,例如,某个动作的撤销(下象棋时悔棋)、保存-些历史记录、做一些快照等。
(9) 备忘录模式主要探讨将数据保存在内存中和从内存中恢复数据,虽然可以将这些数据保存到磁盘上或从磁盘中重新载人数据,但磁盘方面的操作并不属于本模式要探讨的话题,相关的功能读者可以自行扩充。
备忘录实现总结
friend的应用场景。