GoF说明:
在不违反封装的原则下,获取一个对象的内部状态并保留在外部,让对象可以再日后回复之前保留的状态。
模式说明:
不违反封装,也就是开闭原则,但是又把内部细节暴漏出去并做修改,看似有些矛盾,备忘录模式却能做到不矛盾,先看一下备忘录模式的三个重要角色:
记录拥有者(Originator):内部的全部数据就是需要被保存的数据,可以自动将内部数据通过生成Memento同步出来,也可以通过外部传入的Memento更新自身内部数据。内部数据可以通过get、set属性调用。
记录保存者(Memento):用于作为Originator打包保存数据的对象
记录保存者管理(CareTaker):管理和拥有Originator产生的不同Memento,最终在这些Memento中确定一个需要被真正保存的版本。
简单概括:我们使用Originator接收原始数据,当在某个阶段,需要一个可能会被最终保存的备份时,我们使用Originator产生一个Memento并持有Originator所有的数据,并送给CareTaker管理。过程中我们随时让Originator回退到某一个阶段保存的数据,方法时从CareTaker取得一个Memento加载并替换到自己的数据中。而最终,我们调用Originator的保存方法,将原始数据替换。
案例分析:游戏存档
1.需求分析:
单机游戏一般会提供存档功能,让玩家一次可以玩几个档。在某些时候进行存档,防止自己死亡重新开始,或者为了方便刷某些概率事物。
2.代码分析:有考虑做流程图,参考了很多流程发现根本不能很好的解释这个模式的流程,反而会给读者造成很多误解。所以就直接通过代码来解释吧,博主会给每个过程都备注上详细的解释
数据备份对象:GameDataMemeto
public class GameDataMemeto {
public int mLv {get;set;}
public string mName {get;set}
public int mStage {get;set;}
public int mMoney {get;set;}
public GameDataMemeto(int lv,string name,int stage,int money){
mLv = lv;
mName = name ;
mStage = stage;
mMoney = money ;
}
}
真正的数据持有者:GameDataOriginator
public class GameDataOriginator{
public int mLv {get;set;}
public string mName {get;set}
public int mStage {get;set;}
public int mMoney {get;set;}
public void SaveData(){
Playerprefs.setInt("lv",mLv);
Playerprefs.setString("name",mName );
Playerprefs.setInt("stage",mStage );
Playerprefs.setInt("money",mMoney );
}
public void loadData(){
mLv = Playerprefs.getInt('lv');
mName = Playerprefs.getString('name');
mStage = Playerprefs.getInt('stage');
mMoney = Playerprefs.getInt('money');
}
//提供一个重载的方法,从Memento中同步数据
public void loadData(GameDataMemeto memento){
mLv = memento.mLv;
mName = memento.mName ;
mStage = memento.mStage ;
mMoney = memento.mMoney ;
}
//在某个阶段需要备份一次数据时,就调用这个方法产生当前数据的备份
public GameDataMemeto CreateMemeto(){
return new GameDataMemeto (mLv,mName,mStage,mMoney);
}
}
GameDataOriginator有两个主要功能,第一个是加载游戏数据,第二个是在需要的阶段对当前游戏数据进行备份。
记录保存者管理:GameDataTaker
public class GameDataTaker{
public GameDataOriginator mOriginator = new GameDataOriginator ();
//所有版本的数据备份字典
private Dictionary<string,GameDataMemeto> mMementoDic = new Dictionary<string,GameDataMemeto>();
//构造函数中初始化游戏数据
private GameDataTaker(){
mOriginator.loadData();
}
//添加或修改备份版本
public void AddMemento(string version){
DoAddMemento(version, mOriginator.CreateMemeto())
}
private void DoAddMemento(string version, GameDataMemeto memento){
if(mMementoDic.ContainsKey(version)==false){
mMementoDic.Add(version,memento);
}else{
mMementoDic[version] = memento;
}
}
//通过key指定游戏加载到某个版本的数据
public void SetDataVersion(string version){
GameDataMemeto memento = GetMemento(version);
if(memento != null){
mOriginator.loadData(memento);
}
}
//获得某一个版的游戏数据,理论上这个方法不应该公开给其他对象使用
private GameDataMemeto GetMemento(string key){
if(mMementoDic.ContainsKey(key)==false){
return null;
}else{
return mMementoDic[key] ;
}
}
}
GameDataTaker有三个功能:
开放mOriginator 供游戏运行提取数据
加载某个版本的游戏数据
保存某个版本的游戏数据
当然GameDataTaker还需要提供一个序列化所有版本到本地存储中,以及在游戏初始化时读取本例序列化文件把所有版本的游戏数据读取到游戏内存中,这里就不详细赘述了。备忘录模式提供了一个很好的数据备份以及回退的功能。
备忘录模式的其他重点应用:数据库操作
数据备份和回退,除了在本地游戏中的应用之外,如果懂一点数据库知识同学可能立马就会想到数据库的回退功能,是的,mysql的回退就借鉴了备忘录模式。做过服务器开发的同学应该也不陌生,每次请求数据的时候,我们可能会多次修改表中的内容,是不是每次都需要直接对数据库进行操作呢?如果其中有一个步骤出错了,是不是就要立马调用数据库的回退功能呢?这样明显时不合理的,我们可以在请求开始时得到需要操作的数据,在请求顺利结束之后提交所有改变。如果中途出错,我们则不会提交数据,这就是一种备忘录的思想。