一:动机(Motivation)
<1>在软件构建过程中,某些对象的状态在转换过程中,可能由于某种需要,要求程序能够回溯到对象之前处于某个点时的状态。如果使用一些公有接口来让其他对象得到对象的状态,便会暴露对象的细节实现。
<2>如何实现对象状态的良好保存与恢复?但同时又不会因此而破坏对象本身的封装性。
二:意图(Intent)
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
——《设计模式》GoF
三:结构(Structure)
四:结构详解
五:生活中的例子
<1>玩游戏(单机)时,需要经常保存进度,在必要的时候(如gameover),再读取存档,继续游戏(即S/L大法)。
<2>三个对象(与下图对应关系):
游戏 = 游戏角色
进度 = 角色状态存储箱
存取档工具 = 角色状态管理者
六:实现
namespace Test
{
public class 游戏进度
{
public 游戏进度(int attack, int life, int defense)
{
this.attack = attack;
this.life = life;
this.defense = defense;
this.createTime = DateTime.Now;
}
private int attack;
public int Attack { get { return attack; } }
private int life;
public int Life { get { return life; } }
private int defense;
public int Defense { get { return defense; } }
private DateTime createTime;
public DateTime CreateTime { get { return createTime; } }
public override string ToString()
{
return String.Format("{0},生命:{1},攻击:{2},防御:{3}",
this.CreateTime, this.Life, this.Attack, this.Defense);
}
}
public class 游戏
{
public 游戏()
{
this.attack = 100;
this.life = 100;
this.defense = 100;
Console.WriteLine("游戏开始于:{0}", DateTime.Now);
}
private int attack;
public int Attack
{
get { return this.attack; }
}
private int life;
public int Life
{
get { return this.life; }
}
private int defense;
public int Defense
{
get { return this.defense; }
}
public void 查看状态()
{
Console.WriteLine("当前生命力:{0},攻击力:{1},防御力:{2},游戏是否结束:{3}",
this.Life, this.Attack, this.Defense, this.游戏结束);
}
public void 战斗()
{
Console.WriteLine("\t开始战斗!");
System.Threading.Thread.Sleep(3000);
int lifeless = new Random().Next(100);
this.life = this.life - lifeless;
int attackless = new Random().Next(100);
this.attack = this.attack - attackless;
int defenseless = new Random().Next(100);
this.defense = this.defense - defenseless;
Console.WriteLine("本回合战斗结束,损失:生命:{0},攻击:{1},防御:{2}",
this.Life, this.Attack, this.Defense);
}
public bool 游戏结束
{
get { return this.Life <= 0; }
}
public 游戏进度 存档()
{
return new 游戏进度(this.attack, this.life, this.defense);
}
public void 读档(游戏进度 saved)
{
this.life = saved.Life;
this.attack = saved.Attack;
this.defense = saved.Defense;
Console.WriteLine("读档结束:{0}", saved);
}
}
/// <summary>
/// careTaker
/// </summary>
public class 存取档工具
{
private System.Collections.Hashtable saves = new System.Collections.Hashtable();
public void 存档(string 进度编号, 游戏进度 progress)
{
if (!this.saves.ContainsKey(进度编号))
{
this.saves.Add(进度编号, progress);
}
else
{
this.saves[进度编号] = progress;
}
Console.WriteLine("进度已保存,进度编号:{0},进度信息:{1}", 进度编号, progress);
}
public 游戏进度 读档(string 进度编号)
{
Console.WriteLine((this.saves[进度编号]).GetType());
return this.saves[进度编号] as 游戏进度;
}
}
internal class Program
{
static void Main(string[] args)
{
游戏 game = new 游戏();
存取档工具 sltool = new 存取档工具();
游戏进度 progress = game.存档();
sltool.存档("000", progress);
game.查看状态(); //查询初始状态
Console.WriteLine("\n第一次战斗\n");
game.战斗(); //第一次战斗
game.查看状态();
progress = game.存档();
sltool.存档("001", progress);
Console.WriteLine("\n第二次战斗\n");
game.战斗(); //第二次战斗
game.查看状态();
progress = game.存档();
sltool.存档("002", progress);
Console.WriteLine("\n读档:000\n");
progress = sltool.读档("000");
game.读档(progress);
game.查看状态();
Console.WriteLine("\n读档:001\n");
progress = sltool.读档("001");
game.读档(progress);
game.查看状态();
Console.WriteLine("\n读档:002\n");
progress = sltool.读档("002");
game.读档(progress);
game.查看状态();
Console.ReadLine();
}
}
}
实现结果
七:实现要点
<1>让”游戏”自己来创建”进度”;
<2>“存取档工具”只是负责”进度”的读取,并不关心”进度”的内部细节;
<3>根据”进退”来重新初始化”游戏”内部细节的工作,应交给”游戏”自己实现;
<4>“存取档工具”可维护多个”进度”。
八:源码测试
using System.Text;
namespace Test
{
public class Memento
{
public int Life { get; set; }
public override string ToString()
{
return String.Format("当前生命:{0}", this.Life);
}
}
public interface IOriginator
{
void Load(Memento memento);
Memento Save();
}
public class Game : IOriginator
{
private int life = 100;
public void Fight()
{
System.Threading.Thread.Sleep(2000);
this.life -= new Random().Next(100);
}
public override string ToString()
{
return String.Format("当前生命值:{0},游戏结束?{1}", this.life, this.life < 1);
}
public void Load(Memento memento)
{
this.life = memento.Life;
}
public Memento Save()
{
return new Memento()
{
Life = this.life
};
}
}
public class CareTaker
{
private Dictionary<int, Memento> mementos = new Dictionary<int, Memento>();
public void SaveMemento(int id, Memento memento)
{
this.mementos.Add(id, memento);
}
public Memento LoadMemento(int id)
{
return this.mementos[id];
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (var item in this.mementos)
{
sb.AppendFormat("存档ID:{0}---{1}\n", item.Key, item.Value);
}
return sb.ToString();
}
}
internal class Program
{
static void Main(string[] args)
{
IOriginator originator = new Game();
Console.WriteLine(originator);
Memento m = originator.Save();
Console.WriteLine(m);
CareTaker careTaker = new CareTaker();
careTaker.SaveMemento(1, m); //第一次存档
(originator as Game).Fight();
Console.WriteLine(originator);
m = originator.Save();
Console.WriteLine(m);
careTaker.SaveMemento(2, m); //第二次存档
(originator as Game).Fight();
Console.WriteLine(originator);
m = originator.Save();
Console.WriteLine(m);
careTaker.SaveMemento(3, m); //第三次存档
Console.WriteLine("------------------");
Console.WriteLine(careTaker);
///还原
Console.WriteLine("------------------");
Game game = originator as Game;
game.Load(careTaker.LoadMemento(3));
Console.WriteLine(game);
game.Load(careTaker.LoadMemento(2));
Console.WriteLine(game);
game.Load(careTaker.LoadMemento(1));
Console.WriteLine(game);
Console.ReadLine();
}
}
}
测试结果
九:适用性
<1>必须保存一个对象在某一个时刻的(部分)状态,这样以后需要时它才能恢复到先前的状态;
<2>如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。
十:总结
<1>备忘录(Memento)存储原发器(Originator)对象的内部状态,在需要时恢复原发器状态;
<2>Memento模式适用于“由原发器管理,却又必须存储在原发器之外的信息”;
<3>在实现Memento模式中,要防止原发器以外的对象访问备忘录对象。备忘录对象有两个接口,一个为原发器使用的宽接口;一个为其他对象使用的窄接口。
<4>在实现Memento模式时,要考虑拷贝对象状态的效率问题,如果对象开销比较大,可以采用某种增量式改变来改进Memento模式。