一、引言
备忘录模式(Memento)是一种行为设计模式,允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。是为防止数据丢失而对数据进行备份以备将来恢复这些数据所采用的一种设计模式。换句话说,该模式可以将某个时间点的对象内部的状态保存下来,后续在必要时,根据保存的内容将该对象恢复到当时的状态。
二、备忘录模式
仍以一个的单机闯关打斗类游戏为例进行讲解。游戏中每个关卡的末尾都有一个BOSS(游戏中战斗力很强的怪物)等待玩家挑战,只有成功击败该BOSS,玩家才能进人下一关,游戏才能继续进行。但在与其进行的战斗中,玩家所操控的角色非常容易被BOSS击败从而导致整个游戏结束,此时玩家必须从头开始进行游戏。这让玩家挫败感非常强,甚至有部分玩家想要放弃这款游戏。
为了解决这个问题,我们要在快进人游戏关卡末尾之前(快遇到BOSS时),在游戏场景中摆放一个NPC(非玩家角色),玩家可以通过与该NPC进行交互,花费一定的金钱来保存此时此刻的游戏进度,如果玩家在与BOSS战斗中被击败则可以利用游戏中的“载人进度”功能将刚才保存过的游戏进度重新载人,这样玩家就可以立即再次尝试与该BOSS进行战斗而不至于从头开始进行游戏。
本节要解决的问题就是如何保存玩家的游戏进度信息的问题。这里的游戏进度信息是指玩家信息,包括玩家当前的生命值、魔法值、攻击力等信息(其他信息读者可以自行添加,例如玩家在游戏世界中的位置、玩家已经通关的关卡编号等)。本范例直接按照备忘录模式的书写方式来实现。首先设计一个玩家类,为尽量不涉及与本章无关的内容,这里直接用一个Fighter
类而不采用一系列继承自Fighter
类的子类来代表玩家,代码如下:
class Fighter{
public:
Fighter(int life,int magic,int attack)
:m_life(life),m_magic(magic),m_attack(attack){}
private:
//角色属性
int m_life;//生命值
int m_magic;//魔法值
int m_attack;//攻击力
//...
};
为了将游戏进度进行保存,换句话说,为了保存玩家的生命值、魔法值、攻击力等信息,引人一个与Fighter
类相关的备忘录类,取名为FighterMemento
,需要保存的玩家信息可以保存到该备忘录中。FighterMemento
类代码:
//玩家主角相关的备忘录类
class FighterMemento {
private:
//构造函数,用private修饰以防止在外部被随意创建
FighterMemento(int life, int magic, int attack)
:m_life(life), m_magic(magic), m_attack(attack)
{}
private:
//提供一些供Fighter类来访问的接口,用private修饰防止被任意类访问
friend class Fighter;//友元类Fighter可以访问本类的私有成员函数
int getLife() const { return m_life; }
void setLife(int life) { m_life = life; }
int getMagic()const { return m_magic; }
void setMagic(int magic) { m_magic = magic; }
int getAttack()const { return m_attack; }
void setAttack(int attack) { m_attack = attack; }
private:
//玩家主角类中要保存起来的数据,就放在此处
int m_life;//生命值
int m_magic;//魔法值
int m_attack;//攻击力
};
可以看到,上述备忘录类FighterMemento
中大多数字段都是用private
进行修饰,以避免其内部的数据被轻易更改,尤其是将构造函数也用private
修饰,以防止该类对象被随意创建,但是将Fighter
声明为了友元类,这表示Fighter
类对象可以随意访问FighterMemento
,而其他类却不行。
有了备忘录类,就可以将玩家类对象的数据保存到备忘录类对象中。在Fighter
类中,增加public
修饰的如下成员函数,用于将玩家数据写人备忘录以及从备忘录中恢复玩家数据:
//从备忘录中恢复玩家数据
void restoreMomento(FighterMemento* pfm) {
m_life = pfm->getLife();
m_magic = pfm->getMagic();
m_attack = pfm->getAttack();
}
//为测试目的引人的接口,设置玩家的生命值为0(玩家死亡)
void setToDead()
{
m_life = 0;
}
//用于输出一些信息
void displayInfo()
{
cout << "玩家主角当前的生命值、魔法值、攻击力分别为:" << m_life << "," << m_magic << ", " << m_attack << endl;
}
下面我们可以这样调用:
Fighter*p_fighter=new Fighter(800,200,300);
p_fighter->displayInfo();
auto p_fighterMeno=p_fighter->createMemento();
cout<<"开始战斗*****"<<endl;
p_fighter->setToDead();
p_fighter->displayInfo();
cout<<"死亡后恢复******"<<endl;
p_fighter->restoreMomento(p_fighterMeno);
p_fighter->displayInfo();
从结果可以看到,玩家对象在与BOSS战斗之前,率先将自已的信息保存到备忘录对象中去,战斗时一旦自身战败,可以通过备忘录对象取回自己与BOSS战斗之前的信息(生命值、魔法值、攻击力)。
此外,备忘录模式一般还会引人一个管理者(负责人)类(但这并不是必需的)。该管理者类持有一个指向备忘录对象的指针用于存取备忘录对象,取名为FCareTaker
,这样,何时创建备忘录以及保存备忘录指针这些事情,就可以由管理者类负责了。代码如下:
//管理者
class FCareTaker
{
public:
FCareTaker(FighterMemento* pfm) :m_pfm(pfm) {}
//获取指向备忘录对象的指针
FighterMemento* getMemento()
{
return m_pfm;
}
//保存指向备忘录对象的指针
void setMemento(FighterMemento* ptmpfm)
{
m_pfm = ptmpfm;
}
private:
FighterMemento* m_pfm;
};
那么就可以这样调用:
Fighter* p_fighter = new Fighter(800, 200, 300);
p_fighter->displayInfo();
FCareTaker *taker = new FCareTaker(p_fighter->createMemento());
cout << "开始战斗*****" << endl;
p_fighter->setToDead();
p_fighter->displayInfo();
cout << "死亡后 恢复 ******" << endl;
p_fighter->restoreMomento(taker->getMemento());
p_fighter->displayInfo();
- 原发器(Originator):一个普通的类(业务类),但可以创建一个备忘录用于保存其当前的内部状态,后续也可以使用备忘录来恢复其内部的状态,将需要保存内部状态的类称为原发器。原发器可以根据需要决定备忘录将存储自身的哪些内部状态。这里指
Fighter
类。 - 备忘录(Memento):备忘录本身是一个对象,用于存储原发器对象在某个瞬间的内部状态,备忘录的设计一般会参考原发器的设计。除原发器(创建了本备忘录对象的原发器)外,其他对象不应该直接使用(访问或修改)备忘录,或者说原发器与备忘录之间是需要进行信息共享但又不让其他类知道的,所以备忘录的接口一般都使用
private
修饰并将原发器类设置为友元类。备忘录是被动的,只有创建备忘录的原发器才会对它的状态进行赋值和检索,这样就避免了暴露只应该由原发器管理却又必须存储在原发器之外的信息。这里指FighterMemento
类。 - 负责人/管理员(Caretaker):负责保存好备忘录,也可以将备忘录传递给其他对象,但不需要知道备忘录对象的细节,也不能对备忘录中的内容进行操作或检查。这里指
FCareTaker
。
玩家主角类(Fighter
类/原发器)中的信息保存到备忘录中,这称为给玩家主角类(完整的称呼应该是玩家主角类对象)做了一个快照。玩家主角类(Fighter
类/原发器)中的生命值、魔法值、攻击力这些重要的信息都有必要保存在备忘录中,所以在备忘录类(FighterMemento
)中也有生命值、魔法值、攻击力字段与玩家主角类相对应,但这并不意味着所有玩家主角类中的信息都要保存在备忘录中,只需要把玩家主角类中必要的信息保存到备忘录中即可。
备忘录模式结构
可以把备忘录嵌套在原发器中。
也可以把原发器作为备忘录友元。这样原发器也可以访问备忘录的成员变量和方法,即使这些方法被声明为私有。
引人备忘录设计模式的定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。
三、总结
当需要创建对象状态快照来恢复其之前的状态时,可以使用备忘录模式。备忘录模式允许复制对象中的全部状态(包括私有成员变量),并将其独立于对象进行保存。它在处理事务(比如需要在出现错误时回滚一个操作)的过程中也必不可少。
可以同时使用命令模式和备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。也可以同时使用备忘录和迭代器模式来获取当前迭代器的状态, 并且在需要的时候进行回滚。
其独立于对象进行保存。它在处理事务(比如需要在出现错误时回滚一个操作)的过程中也必不可少。
可以同时使用命令模式和备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。也可以同时使用备忘录和迭代器模式来获取当前迭代器的状态, 并且在需要的时候进行回滚。
有时候原型模式可以作为备忘录的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。