一、引言

今天我们开始讲“行为型”设计模式的第十个模式,该模式是【备忘录模式】,英文名称是:Memento Pattern。按老规矩,先从名称上来看看这个模式,个人的最初理解就是对某个对象的状态进行保存,等到需要恢复的时候,可以从备忘录中进行恢复。生活中这样的例子也能经常看到,如备份电话通讯录,备份操作操作系统,备份数据库等。如果我们想恢复对象的状态,那么我们可能首先想到的是把对象保存下来,但是这样会破坏对象的封装性。因为对象有状态有操作,如果我们为了保存对象而留着原来的对象,做一个深拷贝,那么其他对象也能通过这个对象的接口访问这个对象状态,这并不是我们所希望的。而我们需要它的职责只是保存和恢复对象状态,而不应在上面支持对对象状态访问的接口,这就产生了Memento模式。

我们看上图,一个对象肯定会有很多状态,这些状态肯定会相互转变而促进对象的发展,如果要想在某一时刻把当前对象回复到以前某一时刻的状态,这个情况用“备忘录模式”就能很好解决该问题。

二、备忘录模式的详细介绍

2.1、动机(Motivate)

在软件构建过程中,某些对象的状态在转换的过程中,可能由于某种需要,要求程序能够回溯到对象之前处于某个点时的状态。如果使用一些公有接口来让其他对象得到对象的状态,便会暴露对象的细节实现。

  如何实现对象状态的良好保存与恢复,但同时又不会因此而破坏对象本身的封装性?

2.2、意图(Intent)

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态(如果没有这个关键点,其实深拷贝就可以解决问题)。这样以后就可以将该对象恢复到原先保存的状态。                                      ——《设计模式》GoF

2.3、结构图(Structure)

2.4、模式的组成
    
    可以看出,在备忘录模式的结构图有以下角色:
 
    (1)、发起人角色(Originator):记录当前时刻的内部状态,负责创建和恢复备忘录数据。负责创建一个备忘录Memento,用以记录当前时刻自身的内部状态,并可使用备忘录恢复内部状态。Originator【发起人】可以根据需要决定Memento【备忘录】存储自己的哪些内部状态。

(2)、备忘录角色(Memento):负责存储发起人对象的内部状态,在进行恢复时提供给发起人需要的状态,并可以防止Originator以外的其他对象访问备忘录。备忘录有两个接口:Caretaker【管理角色】只能看到备忘录的窄接口,他只能将备忘录传递给其他对象。Originator【发起人】却可看到备忘录的宽接口,允许它访问返回到先前状态所需要的所有数据。

(3)、管理者角色(Caretaker):负责保存备忘录对象。负责备忘录Memento,不能对Memento的内容进行访问或者操作。

2.5、备忘录模式的代码实现

今天我们就用备份电话本的实例来说明备忘录模式的实现。实现代码如下:

 namespace MementoPattern
{
// 联系人--需要备份的数据,是状态数据,没有操作
public sealed class ContactPerson
{
//姓名
public string Name { get; set; } //电话号码
public string MobileNumber { get; set; }
} // 发起人--相当于【发起人角色】Originator
public sealed class MobileBackOriginator
{
// 发起人需要保存的内部状态
private List<ContactPerson> _personList; public List<ContactPerson> ContactPersonList
{
get
{
return this._personList;
} set
{
this._personList = value;
}
}
//初始化需要备份的电话名单
public MobileBackOriginator(List<ContactPerson> personList)
{
if (personList != null)
{
this._personList = personList;
}
else
{
throw new ArgumentNullException("参数不能为空!");
}
} // 创建备忘录对象实例,将当期要保存的联系人列表保存到备忘录对象中
public ContactPersonMemento CreateMemento()
{
return new ContactPersonMemento(new List<ContactPerson>(this._personList));
} // 将备忘录中的数据备份还原到联系人列表中
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
public sealed class ContactPersonMemento
{
// 保存发起人创建的电话名单数据,就是所谓的状态
public List<ContactPerson> ContactPersonListBack { get; private set; } public ContactPersonMemento(List<ContactPerson> personList)
{
ContactPersonListBack = personList;
}
} // 管理角色,它可以管理【备忘录】对象,如果是保存多个【备忘录】对象,当然可以对保存的对象进行增、删等管理处理---相当于【管理者角色】Caretaker
public sealed class MementoManager
{
//如果想保存多个【备忘录】对象,可以通过字典或者堆栈来保存,堆栈对象可以反映保存对象的先后顺序
//比如:public Dictionary<string, ContactPersonMemento> ContactPersonMementoDictionary { get; set; }
public ContactPersonMemento ContactPersonMemento { get; set; }
} class Program
{
static void Main(string[] args)
{
List<ContactPerson> persons = new List<ContactPerson>()
{
new ContactPerson() { Name="黄飞鸿", MobileNumber = ""},
new ContactPerson() { Name="方世玉", MobileNumber = ""},
new ContactPerson() { Name="洪熙官", MobileNumber = ""}
}; //手机名单发起人
MobileBackOriginator mobileOriginator = new MobileBackOriginator(persons);
mobileOriginator.Show(); // 创建备忘录并保存备忘录对象
MementoManager manager = new MementoManager();
manager.ContactPersonMemento = mobileOriginator.CreateMemento(); // 更改发起人联系人列表
Console.WriteLine("----移除最后一个联系人--------");
mobileOriginator.ContactPersonList.RemoveAt();
mobileOriginator.Show(); // 恢复到原始状态
Console.WriteLine("-------恢复联系人列表------");
mobileOriginator.RestoreMemento(manager.ContactPersonMemento);
mobileOriginator.Show(); Console.Read();
}
}
}

代码很简单,注释也很详细,不细说了。

三、备忘录模式的实现要点:

备忘录(Memento)存储原发器(Originator)对象的内部状态,在需要时恢复原发器状态。Memento模式适用于“由原发器管理,却又必须存储在原发器之外的信息”。

  在实现Memento模式中,要防止原发器以外的对象访问备忘录对象。备忘录对象有两个接口,一个为原发器使用的宽接口;一个为其他对象使用的窄接口。在实现Memento模式时,要考虑拷贝对象状态的效率问题,如果对象开销比较大,可以采用某种增量式改变(即只记住改变的状态)来改进Memento模式。

  我们也可以用序列化的方式实现备忘录。序列化之后,我们可以把它临时性保存到数据库、文件、进程内、进程外等地方。

(1)、备忘录模式的主要优点有:

1】、如果某个操作错误地破坏了数据的完整性,此时可以使用备忘录模式将数据恢复成原来正确的数据。

2】、备份的状态数据保存在发起人角色之外,这样发起人就不需要对各个备份的状态进行管理。而是由备忘录角色进行管理,而备忘录角色又是由管理者角色管理,符合单一职责原则。

3】、提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用先前存储起来的备忘录将状态复原。

4】、实现了信息的封装,一个备忘录对象是一种原发器对象的表示,不会被其他代码改动,这种模式简化了原发器对象,备忘录只保存原发器的状态,采用堆栈来存储备忘录对象可以实现多次撤销操作,可以通过在负责人中定义集合对象来存储多个备忘录。
  
            5】、本模式简化了发起人类。发起人不再需要管理和保存其内部状态的一个个版本,客户端可以自行管理他们所需要的这些状态的版本。
  
            6】、当发起人角色的状态改变的时候,有可能这个状态无效,这时候就可以使用暂时存储起来的备忘录将状态复原。

 (2)、备忘录模式的主要缺点有:

1】、在实际的系统中,可能需要维护多个备份,需要额外的资源,这样对资源的消耗比较严重。资源消耗过大,如果类的成员变量太多,就不可避免占用大量的内存,而且每保存一次对象的状态都需要消耗内存资源,如果知道这一点大家就容易理解为什么一些提供了撤销功能的软件在运行时所需的内存和硬盘空间比较大了。

2】、如果发起人角色的状态需要完整地存储到备忘录对象中,那么在资源消耗上面备忘录对象会很昂贵。

      3】、当负责人角色将一个备忘录 存储起来的时候,负责人可能并不知道这个状态会占用多大的存储空间,从而无法提醒用户一个操作是否很昂贵。

      4】、当发起人角色的状态改变的时候,有可能这个协议无效。如果状态改变的成功率不高的话,不如采取“假如”协议模式。

(3)、在下面的情况下可以考虑使用备忘录模式:

1】、如果系统需要提供回滚操作时,使用备忘录模式非常合适。例如文本编辑器的Ctrl+Z撤销操作的实现,数据库中事务操作。

2】、保存一个对象在某一个时刻的状态或部分状态,这样以后需要时它能够恢复到先前的状态。

3】、如果用一个接口来让其他对象得到这些状态,将会暴露对象的实现细节并破坏对象的封装性,一个对象不希望外界直接访问其内部状态,通过负责人可以间接访问其内部状态。

4】、有时一些发起人对象的内部信息必须保存在发起人对象以外的地方,但是必须要由发起人对象自己读取,这时,使用备忘录模式可以把复杂的发起人内部信息对其他的对象屏蔽起来,从而可以恰当地保持封装的边界。

(4)备忘录的封装性

1】、为了确保备忘录的封装性,除了原发器外,其他类是不能也不应该访问备忘录类的,在实际开发中,原发器与备忘录之间的关系是非常特殊的,它们要分享信息而不让其他类知道,实现的方法因编程语言的不同而不同。

(5)多备份实现

1】、在负责人中定义一个集合对象来存储多个状态,而且可以方便地返回到某一历史状态。

2】、在备份对象时可以做一些记号,这些记号称为检查点(Check Point)。在使用HashMap等实现时可以使用Key来设置检查点。

四、.NET 备忘录模式的实现

在现在的Net框架里面,还没有找到备忘录模式的实现,看来还是自己的功力不够,还需努力。个人的理解,这种模式似乎在业务系统里面使用的更多,类似Word,Excel等工具可以有撤销功能,其实很多软件都有这个功能,软件执行的时候,时时刻刻在把自己的状态存储,如果发生错误,或者需要撤销的时候就可以进行相关的操作。

五、总结

备忘录模式写完了,这个模式刚开始理解起来还是挺麻烦的,但是,如果我们多看几个实例代码,完全掌握也不是问题。我们是不是感觉Memento模式和Command模式有些类似,我们要仔细把握模式之间的异同,否则使用模式的时候就会出现张冠李戴的情况或者不能确定使用哪个模式好。Memento备忘录模式和Command命令模式其实还是有些细微的差别的,那就让我们来看看他们的异同吧。虽然两者都支持Undo操作,但是Command是对行为的封装,Memento是对对象状态的保留,这是目的上的不同。它们支持的也是Undo操作的不同层面,Command是对行为序列的操作,Memento是对行为状态的操作。命令模式保存的是发起人的具体命令(命令对应的是行为),而备忘录模式保存的是发起人的状态(而状态对应的数据结构,如属性)。把握细节,理解模式的应用场景,这样可以让模式更好的为我们服务。

C#设计模式之二十二备忘录模式(Memeto Pattern)【行为型】的更多相关文章

  1. 备忘录模式 Memento 快照模式 标记Token模式 行为型 设计模式(二十二)

    备忘录模式 Memento   沿着脚印,走过你来时的路,回到原点.     苦海翻起爱恨   在世间难逃避命运   相亲竟不可接近   或我应该相信是缘份   一首<一生所爱>触动了多少 ...

  2. 二十四种设计模式:备忘录模式(Memento Pattern)

    备忘录模式(Memento Pattern) 介绍在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态.这样以后就可将该对象恢复到保存的状态. 示例有一个Message实体类,某 ...

  3. Java 设计模式系列(二十)状态模式

    Java 设计模式系列(二十)状态模式 状态模式,又称状态对象模式(Pattern of Objects for States),状态模式是对象的行为模式.状态模式允许一个对象在其内部状态改变的时候改 ...

  4. Java 设计模式系列(十二)策略模式(Strategy)

    Java 设计模式系列(十二)策略模式(Strategy) 策略模式属于对象的行为模式.其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换.策略模式使得算法可以 ...

  5. Java设计模式(十) 备忘录模式 状态模式

    (十九)备忘录模式 备忘录模式目的是保存一个对象的某个状态,在适当的时候恢复这个对象. class Memento{ private String value; public Memento(Stri ...

  6. JAVA基础知识总结:一到二十二全部总结

    >一: 一.软件开发的常识 1.什么是软件? 一系列按照特定顺序组织起来的计算机数据或者指令 常见的软件: 系统软件:Windows\Mac OS \Linux 应用软件:QQ,一系列的播放器( ...

  7. Java 设计模式系列(十)外观模式

    Java 设计模式系列(十)外观模式 门面模式(Facade):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这 ...

  8. WCF技术剖析之二十二: 深入剖析WCF底层异常处理框架实现原理[中篇]

    原文:WCF技术剖析之二十二: 深入剖析WCF底层异常处理框架实现原理[中篇] 在[上篇]中,我们分别站在消息交换和编程的角度介绍了SOAP Fault和FaultException异常.在服务执行过 ...

  9. VMware vSphere 服务器虚拟化之二十二桌面虚拟化之创建View Composer链接克隆的虚拟桌面池

    VMware vSphere 服务器虚拟化之二十二桌面虚拟化之创建View Composer链接克隆的虚拟桌面池 在上一节我们创建了完整克隆的自动专有桌面池,在创建过程比较缓慢,这次我们将学习创建Vi ...

  10. Senparc.Weixin.MP SDK 微信公众平台开发教程(二十二):如何安装 Nuget(dll) 后使用项目源代码调试

    最近碰到开发者问:我使用 nuget 安装了 Senparc.Weixin SDK,但是有一些已经封装好的过程想要调试,我又不想直接附加源代码项目,这样就没有办法同步更新了,我应该怎么办? 这其实是一 ...

随机推荐

  1. RPC是什么

    RPC是什么? 通俗的讲就是,调用远程计算机上的服务,就像调用本地服务一样.通常包含传输协议和编码协议. RPC可以基于HTTP或TCP协议,但基于HTTP协议的RPC性能却不如基于TCP协议的RPC ...

  2. JavaScript OOP(三):prototype原型对象(即构造函数的prototype属性)

    通过构造函数生成的实例化对象,无法共享属性或方法(即每个实例化对象上都有构造函数中的属性和方法):造成了一定的资源浪费 function Obj(name,age){ this.name=name; ...

  3. 强大的MobaXterm

    MOobaXterm是一款强大的远程终端登录软件. 1.多终端分屏 2.内建SFTP文件传输(这个功能用的太爽了) 等等 功能强大,还需要继续研究

  4. HDU1248--完全背包

    寒冰王座 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submi ...

  5. HDU 1216 Assistance Required 埃拉托色尼色筛法

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1216 思路:色筛法 代码(1): #include<iostream>//-------- ...

  6. PHP进程锁

    <?php /** * CacheLock 进程锁,主要用来进行cache失效时的单进程cache获取,防止过多的SQL请求穿透到数据库 * 用于解决PHP在并发时候的锁控制,通过文件/eacc ...

  7. 高质量PHP代码的50个实用技巧必备(上)

    1.不要使用相对路径 常常会看到: ? 1 require_once('../../lib/some_class.php'); 该方法有很多缺点: 它首先查找指定的php包含路径, 然后查找当前目录. ...

  8. 深入理解ES6之—增强的数组功能

    创建数组 Array.of()方法 ES6为数组新增创建方法的目的之一,是帮助开发者在使用Array构造器时避开js语言的一个怪异点.Array.of()方法总会创建一个包含所有传入参数的数组,而不管 ...

  9. jmeter接口系列:时间戳、加密

    JMeter安装配置 从官网下载JMeter的软件包apache-jmeter-x.x.zip,下载完成之后解压打开jmeter.bat即可. 说明 这里使用的jmeter版本是3.0,jdk版本是j ...

  10. FPGA编程基础(一)--參数传递与寄存器使用

    一.參数映射 參数映射的功能就是实现參数化元件.所谓的"參数化元件"就是指元件的某些參数是可调的,通过调整这些參数从而可实现一类结构类似而功能不同的电路.在应用中.非常多电路都可採 ...