一、引言
在上一篇博文分享了访问者模式,访问者模式的实现是把作用于某种数据结构上的操作封装到访问者中,使得操作和数据结构隔离。而今天要介绍的备忘者模式与命令模式有点相似,不同的是,命令模式保存的是发起人的具体命令(命令对应的是行为),而备忘录模式保存的是发起人的状态(而状态对应的数据结构,如属性)。下面具体来看看备忘录模式。
二、备忘录模式介绍
2.1 备忘录模式的定义
从字面意思就可以明白,备忘录模式就是对某个类的状态进行保存下来,等到需要恢复的时候,可以从备忘录中进行恢复。生活中这样的例子经常看到,如备忘电话通讯录,备份操作操作系统,备份数据库等。
备忘录模式的具体定义是:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以把该对象恢复到原先的状态。
2.2 备忘录模式的结构图
介绍完备忘录模式的定义之后,下面具体看看备忘录模式的结构图:
备忘录模式中主要有三类角色:
- 发起人角色:记录当前时刻的内部状态,负责创建和恢复备忘录数据。
- 备忘录角色:负责存储发起人对象的内部状态,在进行恢复时提供给发起人需要的状态。
- 管理者角色:负责保存备忘录对象。
2.3 备忘录模式的实现
下面以备份手机通讯录为例子来实现了备忘录模式,具体的实现代码如下所示:
// 联系人
public class ContactPerson
{
public string Name { get; set; }
public string MobileNum { get; set; }
}
// 发起人
public class MobileOwner
{
// 发起人需要保存的内部状态
public List<ContactPerson> ContactPersons { get; set; }
public MobileOwner(List<ContactPerson> persons)
{
ContactPersons = persons;
}
// 创建备忘录,将当期要保存的联系人列表导入到备忘录中
public ContactMemento CreateMemento()
{
// 这里也应该传递深拷贝,new List方式传递的是浅拷贝,
// 因为ContactPerson类中都是string类型,所以这里new list方式对ContactPerson对象执行了深拷贝
// 如果ContactPerson包括非string的引用类型就会有问题,所以这里也应该用序列化传递深拷贝
return new ContactMemento(new List<ContactPerson>(this.ContactPersons));
}
// 将备忘录中的数据备份导入到联系人列表中
public void RestoreMemento(ContactMemento memento)
{
// 下面这种方式是错误的,因为这样传递的是引用,
// 则删除一次可以恢复,但恢复之后再删除的话就恢复不了.
// 所以应该传递contactPersonBack的深拷贝,深拷贝可以使用序列化来完成
this.ContactPersons = memento.contactPersonBack;
}
public void Show()
{
Console.WriteLine("联系人列表中有{0}个人,他们是:", ContactPersons.Count);
foreach (ContactPerson p in ContactPersons)
{
Console.WriteLine("姓名: {0} 号码为: {1}", p.Name, p.MobileNum);
}
}
}
// 备忘录
public class ContactMemento
{
// 保存发起人的内部状态
public List<ContactPerson> contactPersonBack;
public ContactMemento(List<ContactPerson> persons)
{
contactPersonBack = persons;
}
}
// 管理角色
public class Caretaker
{
public ContactMemento ContactM { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<ContactPerson> persons = new List<ContactPerson>()
{
new ContactPerson() { Name= "Learning Hard", MobileNum = "123445"},
new ContactPerson() { Name = "Tony", MobileNum = "234565"},
new ContactPerson() { Name = "Jock", MobileNum = "231455"}
};
MobileOwner mobileOwner = new MobileOwner(persons);
mobileOwner.Show();
// 创建备忘录并保存备忘录对象
Caretaker caretaker = new Caretaker();
caretaker.ContactM = mobileOwner.CreateMemento();
// 更改发起人联系人列表
Console.WriteLine("----移除最后一个联系人--------");
mobileOwner.ContactPersons.RemoveAt(2);
mobileOwner.Show();
// 恢复到原始状态
Console.WriteLine("-------恢复联系人列表------");
mobileOwner.RestoreMemento(caretaker.ContactM);
mobileOwner.Show();
Console.Read();
}
}
具体的运行结果如下图所示:
从上图可以看出,刚开始通讯录中有3个联系人,然后移除以后一个后变成2个联系人了,最后恢复原来的联系人列表后,联系人列表中又恢复为3个联系人了。
上面代码只是保存了一个还原点,即备忘录中只保存了3个联系人的数据,但是,如果想备份多个还原点怎么办呢?即恢复到3个人后,又想恢复到前面2个人的状态,这时候可能你会想,这样没必要啊,到时候在删除不就好了。但是如果在实际应用中,可能我们发了很多时间去创建通讯录中只有2个联系人的状态,恢复到3个人的状态后,发现这个状态时错误的,还是原来2个人的状态是正确的,难道我们又去花之前的那么多时间去重复操作吗?这显然不合理,如果就思考,能不能保存多个还原点呢?保存多个还原点其实很简单,只需要保存多个备忘录对象就可以了。具体实现代码如下所示:
namespace MultipleMementoPattern
{
// 联系人
public class ContactPerson
{
public string Name { get; set; }
public string MobileNum { get; set; }
}
// 发起人
public class MobileOwner
{
public List<ContactPerson> ContactPersons { get; set; }
public MobileOwner(List<ContactPerson> persons)
{
ContactPersons = persons;
}
// 创建备忘录,将当期要保存的联系人列表导入到备忘录中
public ContactMemento CreateMemento()
{
// 这里也应该传递深拷贝,new List方式传递的是浅拷贝,
// 因为ContactPerson类中都是string类型,所以这里new list方式对ContactPerson对象执行了深拷贝
// 如果ContactPerson包括非string的引用类型就会有问题,所以这里也应该用序列化传递深拷贝
return new ContactMemento(new List<ContactPerson>(this.ContactPersons));
}
// 将备忘录中的数据备份导入到联系人列表中
public void RestoreMemento(ContactMemento memento)
{
if (memento != null)
{
// 下面这种方式是错误的,因为这样传递的是引用,
// 则删除一次可以恢复,但恢复之后再删除的话就恢复不了.
// 所以应该传递contactPersonBack的深拷贝,深拷贝可以使用序列化来完成
this.ContactPersons = memento.ContactPersonBack;
}
}
public void Show()
{
Console.WriteLine("联系人列表中有{0}个人,他们是:", ContactPersons.Count);
foreach (ContactPerson p in ContactPersons)
{
Console.WriteLine("姓名: {0} 号码为: {1}", p.Name, p.MobileNum);
}
}
}
// 备忘录
public class ContactMemento
{
public List<ContactPerson> ContactPersonBack {get;set;}
public ContactMemento(List<ContactPerson> persons)
{
ContactPersonBack = persons;
}
}
// 管理角色
public class Caretaker
{
// 使用多个备忘录来存储多个备份点
public Dictionary<string, ContactMemento> ContactMementoDic { get; set; }
public Caretaker()
{
ContactMementoDic = new Dictionary<string, ContactMemento>();
}
}
class Program
{
static void Main(string[] args)
{
List<ContactPerson> persons = new List<ContactPerson>()
{
new ContactPerson() { Name= "Learning Hard", MobileNum = "123445"},
new ContactPerson() { Name = "Tony", MobileNum = "234565"},
new ContactPerson() { Name = "Jock", MobileNum = "231455"}
};
MobileOwner mobileOwner = new MobileOwner(persons);
mobileOwner.Show();
// 创建备忘录并保存备忘录对象
Caretaker caretaker = new Caretaker();
caretaker.ContactMementoDic.Add(DateTime.Now.ToString(), mobileOwner.CreateMemento());
// 更改发起人联系人列表
Console.WriteLine("----移除最后一个联系人--------");
mobileOwner.ContactPersons.RemoveAt(2);
mobileOwner.Show();
// 创建第二个备份
Thread.Sleep(1000);
caretaker.ContactMementoDic.Add(DateTime.Now.ToString(), mobileOwner.CreateMemento());
// 恢复到原始状态
Console.WriteLine("-------恢复联系人列表,请从以下列表选择恢复的日期------");
var keyCollection = caretaker.ContactMementoDic.Keys;
foreach (string k in keyCollection)
{
Console.WriteLine("Key = {0}", k);
}
while (true)
{
Console.Write("请输入数字,按窗口的关闭键退出:");
int index = -1;
try
{
index = Int32.Parse(Console.ReadLine());
}
catch
{
Console.WriteLine("输入的格式错误");
continue;
}
ContactMemento contactMentor = null;
if (index < keyCollection.Count && caretaker.ContactMementoDic.TryGetValue(keyCollection.ElementAt(index), out contactMentor))
{
mobileOwner.RestoreMemento(contactMentor);
mobileOwner.Show();
}
else
{
Console.WriteLine("输入的索引大于集合长度!");
}
}
}
}
}
这样就保存了多个状态,客户端可以选择恢复的状态点,具体运行结果如下所示:
实现深拷贝序列化类:
/// <summary>
/// 序列化和反序列化辅助类
/// </summary>
public class SerializableHelper
{
//序列化
public string Serializable(object target)
{
using (MemoryStream stream = new MemoryStream())
{
new BinaryFormatter().Serialize(stream, target);
return Convert.ToBase64String(stream.ToArray());
}
}
//反序列化
public object Derializable(string target)
{
byte[] targetArray = Convert.FromBase64String(target);
using (MemoryStream stream = new MemoryStream(targetArray))
{
return new BinaryFormatter().Deserialize(stream);
}
}
public T Derializable<T>(string target)
{
return (T)Derializable(target);
}
public T CopyObj<T>(T obj)
{
string target =Serializable(obj);
return Derializable<T>(target);
}
}
修改为:
public ContactMemento CreateMemento()
{
// var reStr = new List<ContactPerson>(this.ContactPersons)
var reStr = new SerializableHelper().CopyObj<List<ContactPerson>>(this.ContactPersons);
return new ContactMemento(reStr);
}
public void RestoreMemento(ContactMemento memento)
{
if (memento != null)
{
// var reStr = new List<ContactPerson>(memento.ContactPersonBack)
var reStr = new SerializableHelper().CopyObj<List<ContactPerson>>(memento.ContactPersonBack);
this.ContactPersons =reStr;
}
}
三、备忘录模式的适用场景
在以下情况下可以考虑使用备忘录模式:
- 如果系统需要提供回滚操作时,使用备忘录模式非常合适。例如文本编辑器的Ctrl+Z撤销操作的实现,数据库中事务操作。
四、备忘录模式的优缺点
备忘录模式具有以下优点:
- 如果某个操作错误地破坏了数据的完整性,此时可以使用备忘录模式将数据恢复成原来正确的数据。
- 备份的状态数据保存在发起人角色之外,这样发起人就不需要对各个备份的状态进行管理。而是由备忘录角色进行管理,而备忘录角色又是由管理者角色管理,符合单一职责原则。
当然,备忘录模式也存在一定的缺点:
- 在实际的系统中,可能需要维护多个备份,需要额外的资源,这样对资源的消耗比较严重。
五、总结
备忘录模式主要思想是——利用备忘录对象来对保存发起人的内部状态,当发起人需要恢复原来状态时,再从备忘录对象中进行获取,在实际开发过程也应用到这点,例如数据库中的事务处理。
JAVA:
备忘录模式又叫做快照模式(Snapshot Pattern)或Token模式,是对象的行为模式。
备忘录模式所涉及的角色有三个:备忘录(Memento)角色、发起人(Originator)角色、负责人(Caretaker)角色。
备忘录(Memento)角色
备忘录角色又如下责任:
(1)将发起人(Originator)对象的内战状态存储起来。备忘录可以根据发起人对象的判断来决定存储多少发起人(Originator)对象的内部状态。
(2)备忘录可以保护其内容不被发起人(Originator)对象之外的任何对象所读取。
备忘录有两个等效的接口:
● 窄接口:负责人(Caretaker)对象(和其他除发起人对象之外的任何对象)看到的是备忘录的窄接口(narrow interface),这个窄接口只允许它把备忘录对象传给其他的对象。
● 宽接口:与负责人对象看到的窄接口相反的是,发起人对象可以看到一个宽接口(wide interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
发起人(Originator)角色
发起人角色有如下责任:
(1)创建一个含有当前的内部状态的备忘录对象。
(2)使用备忘录对象存储其内部状态。
负责人(Caretaker)角色
负责人角色有如下责任:
(1)负责保存备忘录对象。
(2)不检查备忘录对象的内容。
备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。因此这个实现又叫做“白箱实现”。
备忘录角色对发起人(Originator)角色对象提供一个宽接口,而为其他对象提供一个窄接口。这样的实现叫做“黑箱实现”。
在JAVA语言中,实现双重接口的办法就是将备忘录角色类设计成发起人角色类的内部成员类。
在“自述历史”模式里面,发起人角色自己兼任负责人角色。
个人总结:
窄接口:负责人(Caretaker)对象只允许它把备忘录对象传给其他的对象。
宽接口:允许读取所有的数据
白箱模式,给任何对象提供宽接口,内部状态对所有人公开。
黑箱模式,给发起人提供宽接口,其它对象提供窄接口,备忘录状态只对发起人公共。
实现双重接口的办法就是将备忘录角色类设计成发起人角色类的内部成员类。
白箱模式模式,发起人,备忘录,负责人分开,发起人创建,备份,持有状态。备忘录备忘数据。负责人保存备忘录。
备忘录私有类,在发起人里面可以强转访问。
自述历史, 发起人自己保存备忘录
黑箱实现:
public class Originator {
private String state;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
System.out.println("赋值状态:" + state);
}
/**
* 工厂方法,返还一个新的备忘录对象
*/
public MementoIF createMemento(){
return new Memento(state);
}
/**
* 发起人恢复到备忘录对象记录的状态
*/
public void restoreMemento(MementoIF memento){
this.setState(((Memento)memento).getState());
}
private class Memento implements MementoIF{
private String state;
/**
* 构造方法
*/
private Memento(String state){
this.state = state;
}
private String getState() {
return state;
}
private void setState(String state) {
this.state = state;
}
}
}
窄接口MementoIF,这是一个标识接口,因此它没有定义出任何的方法。
public interface MementoIF {
}
负责人角色类Caretaker能够得到的备忘录对象是以MementoIF为接口的,由于这个接口仅仅是一个标识接口,因此负责人角色不可能改变这个备忘录对象的内容。
public class Caretaker {
private MementoIF memento;
/**
* 备忘录取值方法
*/
public MementoIF retrieveMemento(){
return memento;
}
/**
* 备忘录赋值方法
*/
public void saveMemento(MementoIF memento){
this.memento = memento;
}
}
public class Client {
public static void main(String[] args) {
Originator o = new Originator();
Caretaker c = new Caretaker();
//改变负责人对象的状态
o.setState("On");
//创建备忘录对象,并将发起人对象的状态存储起来
c.saveMemento(o.createMemento());
//修改发起人对象的状态
o.setState("Off");
//恢复发起人对象的状态
o.restoreMemento(c.retrieveMemento());
}
}
C# 备忘录简单总结:
个人定义:对象之外保存它的内部状态,以便恢复
1 场景: 提供回滚:比如数据库事务
2 优缺点 1 恢复数据 2 备忘录保存备份 ,单一职责 3 缺点:维护多个备份消耗资源
- 发起人角色:创建和恢复备忘录数据。
- 备忘录角色:1 存储内部状态,恢复时提供状态。2保护内容不被发起人之外读取。
- 管理者(负责人)角色:1 保存备忘录对象。2不检查备忘录的内容。
JAVA总结:
又叫快照或Token模式,是行为模式。
两个接口: 1 窄接口:只允许把备忘录对象传给其他的对象。 2宽接口:允许读取所有的数据
备忘录角色对任何对象都提供宽接口,叫做“白箱实现”。
备忘录角色对发起人提供宽接口,其他对象提供窄接口。叫做“黑箱实现”。
在JAVA,实现双重接口的办法就是将备忘录角色类设计成发起人角色类的内部成员类。
在“自述历史”模式里面,发起人角色自己兼任负责人角色。
参考文献:
https://www.cnblogs.com/java-my-life/archive/2012/06/06/2534942.html