一、备忘录模式(Memento Pattern):
也称为快照模式(Snapshot Pattern)23种设计模式之一,属于行为模式。
定义:在不破坏封闭的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
二、备忘录模式的结构
涉及角色:
• 发起人:负责创建一个备忘录Memento,用以记录当前时刻自身的内部状态,并可使用备忘录恢复内部状态。Originator可以根据需要决定Memento存储自己的哪些内部状态。
• 备忘录:负责存储Originator对象的内部状态,并可以防止Originator以外的其他对象访问备忘录。备忘录有两个接口:Caretaker只能看到备忘录的窄接口,他只能将备忘录传递给其他对象。Originator却可看到备忘录的宽接口,允许它访问返回到先前状态所需要的所有数据。
为了控制对备忘录对象的访问,备忘录模式中出现了窄接口和宽接口的概念。
• 窄接口:管理者只能看到备忘录的窄接口,窄接口的实现中通常没有任何的方法,只是一个类型标识。窄接口使得管理者只能将备忘录传递给其他对象。
• 宽接口:原发器能够看到备忘录的宽接口,从而可以从备忘录中获取到所需的数据,来将自己恢复到备忘录中所保存的状态。理想情况是:只允许生成备忘录的原发器来访问该备忘录的内部状态,通常实现成为原发器内的一个私有内部类。
• 管理者:负责备忘录Memento,不能对Memento的内容进行访问或者操作。
简单实现备忘录的基本结构:
备忘录窄接口定义:
发起者(Originator):通过创建一个新的备忘录对象来保存自己的内部状态 示例代码如图:
备忘录管理者(Caretaker):负责保存备忘录对象,但是从不修改(甚至不查看)备忘录对象的内容。
创建一个客户端测试 示例代码如图:
运行程序打印结果如下:
备忘录模式具有封闭性,对于状态得存储只有发起者知道。
三、备忘录模式的使用场景
• 需要保存和恢复数据的相关状态场景。
• 提供一个可回滚(rollback)的操作。
• 数据库连接的事务管理就是用的备忘录模式。
提出几个实例方便我们理解:
•浏览器回退:浏览器一般有浏览记录,当我们在一个网页上点击几次链接之后,可在左上角点击左箭头回退到上一次的页面,然后也可以点击右箭头重新回到当前页面
• 数据库备份与还原:一般的数据库都支持备份与还原操作,备份即将当前已有的数据或者记录保留,还原即将已经保留的数据恢复到对应的表中
•编辑器撤销与重做:在编辑器上编辑文字,写错时可以按快捷键 Ctrl + z 撤销,撤销后可以按 Ctrl + y 重做
• 虚拟机生成快照与恢复:虚拟机可以生成一个快照,当虚拟机发生错误时可以恢复到快照的样子
•Git版本管理:Git是最常见的版本管理软件,每提交一个新版本,实际上Git就会把它们自动串成一条时间线,每个版本都有一个版本号,使用 git reset --hard 版本号 即可回到指定的版本,让代码时空穿梭回到过去某个历史时刻
•棋牌游戏悔棋:在棋牌游戏中,有时下快了可以悔棋,回退到上一步重新下。
示例1:在棋牌游戏中可以悔棋,即重新退回到上一步重新下棋。
棋子类 Chessman原发器角色:
@Data
@AllArgsConstructor
class Chessman {
private String label;
private int x;
private int y;
//保存状态
public ChessmanMemento save() {
return new ChessmanMemento(this.label, this.x, this.y);
}
//恢复状态
public void restore(ChessmanMemento memento) {
this.label = memento.getLabel();
this.x = memento.getX();
this.y = memento.getY();
}
public void show() {
System.out.println(String.format("棋子<%s>:当前位置为:<%d, %d>", this.getLabel(), this.getX(), this.getY()));
}
}
备忘录角色 ChessmanMemento:
@Data
@AllArgsConstructor
class ChessmanMemento {
private String label;
private int x;
private int y;
}
负责人角色 MementoCaretaker:
class MementoCaretaker {
//定义一个集合来存储备忘录
private ArrayList mementolist = new ArrayList();
public ChessmanMemento getMemento(int i) {
return (ChessmanMemento) mementolist.get(i);
}
public void addMemento(ChessmanMemento memento) {
mementolist.add(memento);
}
}
棋子客户端,维护MementoCaretaker 对象:
class Client{
private static int index = -1;
private static MementoCaretaker mc = new MementoCaretaker();
public static void main(String args[]) {
Chessman chess = new Chessman("车", 1, 1);
play(chess);
chess.setY(4);
play(chess);
chess.setX(5);
play(chess);
undo(chess, index);
undo(chess, index);
redo(chess, index);
redo(chess, index);
}
//下棋,同时保存备忘录
public static void play(Chessman chess) {
mc.addMemento(chess.save());
index++;
chess.show();
}
//悔棋,撤销到上一个备忘录
public static void undo(Chessman chess, int i) {
System.out.println("******悔棋******");
index--;
chess.restore(mc.getMemento(i - 1));
chess.show();
}
//撤销悔棋,恢复到下一个备忘录
public static void redo(Chessman chess, int i) {
System.out.println("******撤销悔棋******");
index++;
chess.restore(mc.getMemento(i + 1));
chess.show();
}
}
运行结果:
程序类图:
示例2:备份电话本
联系人:需要备份的数据,是状态数据,没有操作
public sealed class ContactPerson
{
//姓名 public string Name { get; set; }
//电话号码 public string MobileNumber { get; set; }
}
发起人:
public sealed class MobileBackOriginator
{
// 发起人需要保存的内部状态 private List
public List
{
get {
return this._personList;
}
set {
this._personList = value;
}
}
//初始化需要备份的电话名单 public MobileBackOriginator(List
{
if (personList != null)
{
this._personList = personList;
}
else {
throw new ArgumentNullException("参数不能为空!");
}
}
// 创建备忘录对象实例,将当期要保存的联系人列表保存到备忘录对象中 public ContactPersonMemento CreateMemento()
{
return new ContactPersonMemento(new List
}
// 将备忘录中的数据备份还原到联系人列表中 public void RestoreMemento(ContactPersonMemento memento)
{
this.ContactPersonList = memento.ContactPersonListBack;
}
public void Show()
{
Console.WriteLine("联系人列表中共有{0}个人,他们是:", ContactPersonList.Count);
foreach (ContactPerson p in ContactPersonList)
{
Console.WriteLine("姓名: {0} 号码: {1}", p.Name, p.MobileNumber);
}
}
}
备忘录对象:用于保存状态数据,保存的是当时对象具体状态数据>>相当于备忘录角色(Memeto)
publicsealedclass ContactPersonMemento
{
// 保存发起人创建的电话名单数据,就是所谓的状态publicList ContactPersonListBack {get;privateset; }
publicContactPersonMemento(List personList)
{
ContactPersonListBack = personList;
}
}
管理角色:它可以管理备忘录对象,如果是保存多个备忘录对象,可以对保存的对象进行增、删等管理处理>>相当于管理者角色(Caretaker)
public sealed class MementoManager
{
//如果想保存多个备忘录对象,可以通过字典或者堆栈来保存,堆栈对象可以反映保存对象的先后顺序
//比如:public Dictionary
}
客户端代码:
class Program
{
static void Main(string[] args)
{
List
{
new ContactPerson() { Name="小明", MobileNumber = "11111111"},
new ContactPerson() { Name="小红", MobileNumber = "22222222"},
new ContactPerson() { Name="小华", MobileNumber = "33333333"}
};
//手机名单发起人 MobileBackOriginator mobileOriginator = new MobileBackOriginator(persons);
mobileOriginator.Show();
// 创建备忘录并保存备忘录对象 MementoManager manager = new MementoManager();
manager.ContactPersonMemento = mobileOriginator.CreateMemento();
// 更改发起人联系人列表 Console.WriteLine("----移除最后一个联系人--------");
mobileOriginator.ContactPersonList.RemoveAt(2);
mobileOriginator.Show();
// 恢复到原始状态 Console.WriteLine("-------恢复联系人列表------");
mobileOriginator.RestoreMemento(manager.ContactPersonMemento);
mobileOriginator.Show();
Console.Read();
}
}
四、备忘录模式的适用性
在以下条件下可以考虑使用备忘录模式:
• 如果必须保存一个对象在某一个时刻的全部或部分状态,方便在以后需要的时候,可以把该对象恢复到先前的状态。
• 如果需要保存一个对象的内部状态,但是如果用接口来让其它对象直接得到这些需要保存的状态,将会暴露对象的实现细节并破坏对象的封装性,这时可以使用备忘录模式,把备忘录对象实现成为原发器对象的私有内部类,从而保证只有原发器对象才能访问该备忘录对象。这样既保存了需要保存的状态,又不会暴露原发器对象的内部实现细节。
主要优点:
• 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
• 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
• 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
• 更好的封装性:备忘录模式通过使用备忘录对象,来封装原发器对象的内部状态,虽然这个对象是保存在原发器对象的外部,但是由于备忘录对象的窄接口并不提供任何方法。这样有效地保证了对原发器对象内部状态的封装,不把原发器对象的内部实现细节暴露给外部。
• 简化了原发器:在备忘录模式中,原发器不再需要管理和保存其内部状态的一个个版本,而是交由管理者或客户端对这些状态的版本进行管理,从而让原发器对象得到简化。
• 窄接口和宽接口:备忘录模式,通过引入窄接口和宽接口,使得不同的地方,对备忘录对象的访问是不一样的。窄接口保证了只有原发器才可以访问备忘录对象存储的状态。
主要缺点:
• 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
• 如果发起人角色的状态需要完整地存储到备忘录对象中,那么在资源消耗上面备忘录对象会很昂贵。
• 当负责人角色将一个备忘录 存储起来的时候,负责人可能并不知道这个状态会占用多大的存储空间,从而无法提醒用户一个操作是否很昂贵。
• 当发起人角色的状态改变的时候,有可能这个协议无效。如果状态改变的成功率不高的话,不如采取“假如”协议模式。
五、备忘录模式的实现
• 增量存储: 如果需要频繁地创建备忘录对象,而且创建和应用备忘录对象来恢复状态的顺序是可控的,那么可以让备忘录进行增量存储,也就是备忘录可以仅仅存储原发器内部相对于上一次存储状态后的增量改变。
• 结合原型模式:在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。
• 离线存储:备忘录的数据可以实现成为离线存储,除了存储在内存中,还可以把备忘录数据存储到文件中、XML中、数据库中,从而支持跨越会话的备份和恢复功能。
六、总结
备忘录模式的功能,首先是在不破坏封装性的前提下,捕获一个对象的内部状态。同时注意:一个是不破坏封装性,即对象不能暴露它不应该暴露的细节;另外一个是捕获的是对象的内部状态,而且通常还是运行期间某个时刻对象的内部状态。
之所以要捕获这些内部状态,是为了在以后的某个时候,可以将该对象的状态恢复到备忘录所保存的状态,这才是备忘录真正的目的。前面保存状态就是为了后面恢复,虽然不是一定要恢复,但是目的是为了恢复。
在备忘录模式中,备忘录对象通常用来记录原发器中需要保存的内部状态,为了不破坏原发器对象的封装性,一般只让原发器自己来操作它的备忘录对象。为了保证这一点,通常会把备忘录对象作为原发器对象的内部类来实现,而且实现成私有的,这样就断了外部来访问这个备忘录对象的途径。