备忘录模式 Memento
 
沿着脚印,走过你来时的路,回到原点。
 
 
苦海翻起爱恨
 
在世间难逃避命运
 
相亲竟不可接近
 
或我应该相信是缘份
 
一首《一生所爱》触动了多少人的心弦,一段五百年都没有结果的爱情,让多少人潸然泪下。
有人说:当你真正看懂了《大话西游》,你就真的懂了爱情;那么,当你听懂了《一生所爱》,你就真的懂了什么是哀伤和无奈。
《大话西游》是不可超越的经典,在《月光宝盒》中,至尊宝为了救白晶晶,使用月光宝盒使时光倒流,几次后产生故障,竟将其带回五百年前,这时紫霞仙子(朱茵饰)向她走来......
 
百度百科中关于词条“VMware快照”的描述
磁盘“快照”是虚拟机磁盘文件(VMDK)在某个点即时的复本。
系统崩溃或系统异常,你可以通过使用恢复到快照来保持磁盘文件系统和系统存储
当升级应用和服务器及给它们打补丁的时候,快照是救世主。VMware快照是VMware Workstation里的一个特色功能。
 
 

 
“脚印”记录了我们来时的路,所以我们可以原路返回;
“月光宝盒”记录了时间的状态,所以至尊宝可以时光穿梭;
“VMware快照”记录了虚拟机磁盘文件(VMDK)在某个点的状态,所以可以通过快照进行系统恢复;
以上,他们都记录了状态,进而可以恢复到原来的状态

意图

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
这样以后就可以将该对象恢复到原先保存的状态。
别名:快照模式或者token标记模式
 
在程序开发过程中,有时可能需要记录对象的状态,以便允许用户取消不确定的操作或者从错误恢复过来
对象的某个瞬时状态,叫做快照(如同你去拍证件照,立等可取,快速的拍照,俗称快照)
每一个快照,通常叫做一个检查点
想要实现快照和撤销恢复机制,就必须将对象的状态保存起来。
这就是备忘录模式的初衷
 
面向对象的三大特征之一就是封装,对象往往封装了他的状态信息,使之与外部的对象隔离
这部分信息通常都是私有的private,不能被外部对象访问,也就是说不能在外部进行保存
如果暴露内部属性比如设置为public,可以外部保存,但是这又违反了封装的原则。
换句话说,封装起来,外部就不能直接访问,但是我们又需要将他的状态保存在外部
而且还不想破坏封装性,也不想暴露内部属性。
 
也就是如何将对象的状态保存在外部,但是外部却仍旧不知道对象的属性细节
 
这个问题类似于:你想把你的一堆私人物品找个地方保存,但是你又不想别人看到你里面的东西怎么办?
最简单的方式就是借助于行李箱
大家都坐过火车,在车站都有箱包寄存处
我们把所有的物品锁到行李箱中,行李箱寄存起来,管理员负责管理我们的行李箱,他并不知道我们箱子里面装的到底是什么
当我们回来取箱子时,管理员将箱子交还给我们,他自始至终都不知道箱子里是什么
 
在程序中,我们应该如何解决这个问题呢?也就是又能外部保存,又不破坏封装?
备忘录模式就是解决这种场景问题的,通过引入备忘录角色Memento和控制外界对他的访问来解决。

结构

原发器角色Originator
也叫做发起者,原发器是需要被保存状态的角色,也就是我们的业务逻辑对象
他创建备忘录Memento对象,也就是创建快照,并且负责借助于备忘录对象Memento恢复状态
 
备忘录角色Memento
负责记录Originator的部分或者全部状态,也就是拥有Originator的部分或者全部属性
备忘录角色的设计必然要参考Originator
 
备忘录角色应该仅仅允许Originator对其进行修改访问,其他包括CareTaker只能够对备忘录进行传递
备忘录角色就是前文的“行李箱”
 
管理员角色CareTaker
负责管理保存备忘录
 
上面说到备忘录模式的解决问题思路“引入备忘录角色Memento和控制外界对他的访问来解决
具体为:
  • 备忘录模式通过引入备忘录Memento记录对象的内部状态
  • 引入管理员CareTaker对备忘录进行管理
  • 控制访问--原发器Originator与备忘录对象进行交互,其他所有地方都只是获取传递,不更改设置Memento的状态
也就是通过备忘录对象对原始对象需要保存的内部状态进行封装
而且,控制对他的访问,外人不能访问内部
这样借助于备忘录对象这个中间层就实现了“在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态
 
对于备忘录对象访问的控制是必须的,如果外界可以随意访问变更备忘录对象
某种程度上来说相当于可以更改原始对象的私有属性了
这是不安全的
 
备忘录模式将“内部状态”与“外界保存”进行解耦,通过中间层备忘录对象来连接。

代码示例

一个简单的快照实现

有一个业务逻辑类Originator,快照主要是对他的状态,也就是内部state属性,进行备份
快照信息保存在备忘录Memento中
通过管理员CareTaker进行保管
package memento.simple;
/**
* 业务逻辑类,也就是我们需要备份的对象
* 内部拥有state属性,用来表示快照需要保存的状态数据
*/
public class Originator {
private Integer state;
/**
* 创建快照备份
* @return
*/
public Memento createMemento() {
return new Memento(state);
}
/**
* 恢复快照
* @param memento
*/
public void recovery(Memento memento) {
state = memento.getState();
} public Integer getState() {
return state;
} public void setState(Integer state) {
this.state = state;
} @Override
public String toString() {
final StringBuilder sb = new StringBuilder("Originator{");
sb.append("state=").append(state);
sb.append('}');
return sb.toString();
}
}
package memento.simple;
/**
* 备忘录类用来保存业务逻辑对象的状态(原发器)
* 备忘录类的属性要参考原发器的设计,确定需要保存哪些数据信息
* 此处我们以state为演示
* 备忘录类提供了getter和setter方法
*/
public class Memento {
private Integer state;
Memento(Integer state) {
this.state = state;
} public Integer getState() {
return state;
} public void setState(Integer state) {
this.state = state;
}
}
package memento.simple;
/**
* 管理员类,内部拥有一个memento,可以设置和获取这个属性
*/
public class CareTaker { private Memento memento;
public Memento getMemento() {
return memento;
} public void setMemento(Memento memento) {
this.memento = memento;
}
}
package memento.simple;
public class Test { public static void main(String[] args) { //创建业务逻辑对象,设置状态信息
Originator originator = new Originator();
originator.setState(2); //快照
Memento memento = originator.createMemento();
CareTaker careTaker = new CareTaker();
careTaker.setMemento(memento); System.out.println("初始时状态: " + originator.toString()); originator.setState(3);
System.out.println("更新状态后: " + originator.toString()); originator.setState(8);
System.out.println("更新状态后: " + originator.toString()); originator.setState(6);
System.out.println("更新状态后: " + originator.toString()); originator.recovery(careTaker.getMemento());
System.out.println("恢复状态后: " + originator.toString());
}
}
 
 
上面的示例中,备忘录Memento对象封装保存原发器角色Originator的状态,管理员CareTaker对备忘录进行管理 
Memento内部的属性state,尽管我们设置了公有的访问器 getter和setter,但是CareTaker以及外界并没有对Memento进行访问
我们是借助于代码逻辑实现了:
  • Memento备忘录对来自Originator的修改开放(比如上面的Originator可以创建Memento)
  • Memento备忘录对来自CareTaker的修改开放(上面示例中,仅仅传递Memento,不曾访问内部细节)
这种模式又被称为白盒模式,因为Memento备忘录大家都看得到,而且都能够对她进行设置处理
我们是借助于“大家写代码的时候都不要访问修改Memento备忘录对象的属性”这一要求完成的
可能一不留神,某个人的代码中就修改了Memento备忘录对象。
说白了,这种对原发器开放,对外界关闭的限制不是强制的,容易出问题
 
怎么才能够强制将备忘录类仅仅对原发器开放,对外界关闭?
我们可以定义两个接口,Open Close用于描述开放和关闭(close接口意为对外界封闭的接口)
所有的对Memento的操作方法,全部定义在Open中,Close不定义任何方法,也就是标记接口
给原发器Originator使用接口Open,外部所有的地方使用Close类型
这样所有使用Close的地方,都不能够修改Memento对象
这样在原来的基础上,将限制增强了,只要是Close类型,那么就不能修改Originator了
不过显然,Open接口也不是私有的,外界仍旧可以使用,所以只是比原来稍微强化一点
并不能彻底解决

内部类方式重构

在java中,可以借助内部类的形式
因为内部类与外部类是友好的
另外与Close接口结合使用,实现对外界的关闭
 
标记接口,外界看到的始终是标记接口,无方法可用
package memento;
public interface MementoInterface {
}
原发器,将Memento放置在内部,对外界仅仅返回标记接口
只有内部才拥有真正的Memento类型
package memento;
/**
* 业务逻辑类,也就是我们需要备份的对象 内部拥有state属性,用来表示快照需要保存的状态数据
*/
public class Originator {
private Integer state;
/**
* 创建快照备份返回标记接口,以使外界不能操作备忘录
*/
public Memento createMemento() {
return new Memento(state);
}
/**
* 恢复快照,接受MementoInterface类型参数 使用时强转为内部类Memento
*/
public void recovery(MementoInterface memento) {
state = ((Memento) memento).getState();
}
public Integer getState() {
return state;
}
public void setState(Integer state) {
this.state = state;
}
/**
* 私有内部类,实现MementoInterface接口,标记接口 用以外界交互,达到对外界close的效果
*/
private class Memento implements MementoInterface {
private Integer state; Memento(Integer state) {
this.state = state;
} public Integer getState() {
return state;
} public void setState(Integer state) {
this.state = state;
}
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Originator{");
sb.append("state=").append(state);
sb.append('}');
return sb.toString();
}
}
外界仅仅接触到MementoInterface类型
CareTaker维护memento
package memento;
/**
* 管理员类,内部拥有一个 MementoInterface,可以设置和获取这个属性
*/
public class CareTaker {
private MementoInterface memento;
public MementoInterface getMemento() {
return memento;
}
public void setMemento(MementoInterface memento) {
this.memento = memento;
}
}
重构后的打印结果如上图所示与原来一致,但是外界却不能修改备忘录对象,备忘录仅仅对原发器开放
 
上图为重构后的结构
Memento作为Originator的内部类,有外部类的引用
隐藏了Open接口,或者说Memento自身也代表了Open接口角色,提供方法可以给外部类Originator使用
CareTaker通过MementoInterface交互,无法访问Memento内部属性
客户端访问Originator和CareTaker,借助于CareTaker管理CareTaker创建的快照

时序图

时序图上半部分(1,2,3)为快照保存,下半部分(4,5)为快照恢复
步骤解析:
  1. 客户端程序对原发器Originator进行状态设置
  2. 客户端程序对原发器进行快照创建
  3. 客户端程序对快照进行保存
  4. 客户端程序获得快照
  5. 客户端程序根据快照进行状态恢复

重构小结

重构的逻辑为:
  • 借助于私有内部类实现了对外界的封闭
  • 将负责人管理员CareTaker与Memento进行解耦,通过抽象MementoInterface进行连接
也就是使用MementoInterface作为外界与Memento访问的中间层
 
更加合理的实现了“在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态”
 
多版本快照
上面的示例中,CareTaker仅仅定义了一个内部属性  private MementoInterface memento;
也就是只能保存一种状态,实际情况下显然不会如此简单,所以可以借助于集合框架对快照进行管理
比如使用HashMap(String,MementoInterface)进行快照版本控制,key可以为版本号

形式变换

CareTaker管理功能的增强
在上面重构后的示例中,客户端对Originator进行操作,并且负责快照的创建以及保存
以及恢复快照时的快照读取
也就是说客户端程序自身协调Originator与CareTaker,CareTaker仅仅负责快照的保存
可以考虑将客户端程序的功能封装到CareTaker中
其余部分不变,仅仅调整CareTaker
内部新增了属性Originator,通过构造方法注入
新增了创建快照和恢复的方法,底层依赖Originator  这就实现了客户端功能的封装
package memento.refactor1;
/**
* 管理员类,内部拥有一个 MementoInterface,可以设置和获取这个属性
*/
public class CareTaker { private MementoInterface memento;
private Originator originator; CareTaker(Originator originator) {
this.originator = originator;
} /**
* 创建快照,借助于内部的Originator
*/
public MementoInterface createMemento() {
return originator.createMemento();
} /**
* 恢复快照,借助于内部的Originator
*/
public void recovery(MementoInterface memento) {
originator.recovery(memento);
}
public MementoInterface getMemento() {
return memento;
}
public void setMemento(MementoInterface memento) {
this.memento = memento;
}
}
变种形式实现了CareTaker对Originator的完全管理,而不仅仅是保存快照
简化的CareTaker
careTaker用于保存快照
或者可以变形为大保姆,不仅仅保存快照,也用于Originator快照的创建与恢复
有些场景下,也可以省略CareTaker
在我们上面的示例测试主函数中,一边创建了快照,很快下面就用到了快照进行恢复
这种场景,显然没有必要专门的安排管理员,可以省略CareTaker角色
伪代码:
public static void main(String[] args) {
Memento memento = originator.createMemento();
originator.recovery(originator.recovery();
}
Originator与Memento合并
备忘录模式的根本在于状态的保存,模式的结构是一种通用的解决方案
Memento对象是对于Originator内部部分或者全部状态的一个封装
如果只是保存对象的状态,也可以借助于原型模式进行替代
也就是说使用另外一个新的Originator对象保存当前Originator对象的状态,这与白盒方式的备忘录模式效果一样

与命令模式的联系

命令模式中将“方法调用”这种命令行为或者说请求 进一步的抽象
封装为一个命令对象,从而可以达到对请求记录的功能,进而可以实现撤销操作。
备忘录模式也是为了记录对象的内部状态,为实现撤销操作做基础工作。
 
命令模式侧重于请求历史的日志记录,能够记录操作的轨迹,然后调用命令接收者的撤销方法
而撤销方法基本上需要对象的内部状态
备忘录模式则是记录对象的状态变化
一个是宏观一个是微观
 
所以说,备忘录模式与命令模式不是可替代关系,而是可以相互配合的好基友
命令模式记录操作历史,备忘录模式记录对象的状态,一起配合实现撤销的操作。

总结

备忘录模式,模式如其名,就是记录状态信息。
备忘录模式引入备忘录角色将对象的“内部状态”与“外部保存”进行解耦
通过对备忘录角色的访问限制,达到备忘录对原发器Originator开放,对外界关闭的效果
 
备忘录模式的重点在于对于Memento的访问控制---对Originator开放,对外界关闭
也就是通常所谓的双接口支持,就是双重标准嘛
也是一种设计思维为了系统安全,提供一个标记接口给外部访问。能够保障系统的安全。
 
通过对状态的记录,提供了状态恢复的实现机制。
每个备忘录对象都是Originator对象的一个瞬时状态,通过对状态变化轨迹的记录,可以实现多次状态轨迹的还原
而且,复杂场景,还可以与命令模式结合使用。
 
缺点也是明显的,对于Originator对象的每一个瞬时状态都使用Memento对象进行保存
如果Originator对象本身很占用资源,那么势必导致占用过多的内存空间,这都是一个个的对象
 
当你需要保存一个对象的瞬时的部分或者全部状态时,可以考虑使用备忘录模式
 

备忘录模式 Memento 快照模式 标记Token模式 行为型 设计模式(二十二)的更多相关文章

  1. Java设计模式(十二) 策略模式

    原创文章,同步发自作者个人博客,http://www.jasongj.com/design_pattern/strategy/ 策略模式介绍 策略模式定义 策略模式(Strategy Pattern) ...

  2. 解释器模式 Interpreter 行为型 设计模式(十九)

      解释器模式(Interpreter)   考虑上图中计算器的例子 设计可以用于计算加减运算(简单起见,省略乘除),你会怎么做?    你可能会定义一个工具类,工具类中有N多静态方法 比如定义了两个 ...

  3. 命令模式 Command 行为型 设计模式(十八)

    命令模式(Command) 请分析上图中这条命令的涉及到的角色以及执行过程,一种可能的理解方式是这样子的: 涉及角色为:大狗子和大狗子他妈 过程为:大狗子他妈角色 调用 大狗子的“回家吃饭”方法 引子 ...

  4. 桥接模式 桥梁模式 bridge 结构型 设计模式(十二)

      桥接模式Bridge   Bridge 意为桥梁,桥接模式的作用就像桥梁一样,用于把两件事物连接起来   意图 将抽象部分与他的实现部分进行分离,使得他们都可以独立的发展.  意图解析 依赖倒置原 ...

  5. C#设计模式之二十二备忘录模式(Memento Pattern)【行为型】

    一.引言 今天我们开始讲“行为型”设计模式的第十个模式,该模式是[备忘录模式],英文名称是:Memento Pattern.按老规矩,先从名称上来看看这个模式,个人的最初理解就是对某个对象的状态进行保 ...

  6. C#设计模式之二十二备忘录模式(Memeto Pattern)【行为型】

    一.引言   今天我们开始讲"行为型"设计模式的第十个模式,该模式是[备忘录模式],英文名称是:Memento Pattern.按老规矩,先从名称上来看看这个模式,个人的最初理解就 ...

  7. Java进阶篇设计模式之十二 ---- 备忘录模式和状态模式

    前言 在上一篇中我们学习了行为型模式的策略模式(Strategy Pattern)和模板模式(Template Pattern).本篇则来学习下行为型模式的两个模式,备忘录模式(Memento Pat ...

  8. Java设计模式之十二 ---- 备忘录模式和状态模式

    前言 在上一篇中我们学习了行为型模式的策略模式(Strategy Pattern)和模板模式(Template Pattern).本篇则来学习下行为型模式的两个模式,备忘录模式(Memento Pat ...

  9. C#设计模式之十二享元模式(Flyweight)【结构型】

    一.引言   今天我们要讲[结构型]设计模式的第六个模式,该模式是[享元模式],英文名称是:Flyweight Pattern.还是老套路,先从名字上来看看."享元"是不是可以这样 ...

随机推荐

  1. PHP全栈学习笔记13

    php与ajax技术 web2.0的到来,ajax逐渐成为主流,什么是ajax,ajax的开发模式,优点,使用技术.(ajax概述,ajax使用的技术,需要注意的 问题,在PHP应用ajax技术的应用 ...

  2. 跟我一起学opencv 第四课之图像的基本操作

    1.图像是由像素组成的,所以修改了像素就可以实现图像的改变. 2先看灰度图像(单通道): *****2.获取灰度图像的像素值使用:  int gray = gray_src.at<uchar&g ...

  3. WAF开放规则定义权:专家策略+用户自定义策略=Web安全

    在第一期“漫说安全”栏目中,我们用四格漫画的形式介绍了基于深度学习的阿里云WAF到底智能在哪里,能帮客户解决什么问题. 在今天的这期栏目里,我们依然通过漫画这种通俗易懂的方式,与大家分享阿里云WAF的 ...

  4. Django-restframework 之认证源码分析

    Django-restframework 源码分析之认证 前言 最近学习了 django 的一个 restframework 框架,对于里面的执行流程产生了兴趣,经过昨天一晚上初步搞清楚了执行流程(部 ...

  5. 立即执行函数 IIFE

    立即执行函数表达式IIFE(Immediately-invoked function expression)我们知道,在javascript(ES5)中,是没有块级作用域的概念的.看一个例子 for ...

  6. K3数据字典备查

    select distinct f.FNumber as 系统代码, f.FName AS 系统名称,  d.FTableName AS 表名,d.FDescription AS 表说明,a.[nam ...

  7. ArcEngine GroupLayer监听图层改变

    最近项目中需要用到在TOC监听图层的变化,从而针对添加的不同图层进行不同的操作.但是当TOC中添加图层组时,无法监听到图层组中添加图层的动作.也就无法获取到向图层组中添加的图层. 在开发手册中也没有找 ...

  8. Git:三、工作原理

    首先,我们对工作区也就是文件夹中的文档进行修改. 然后,把修改并需要存档的文档用add命令放到暂存区,并且可以放很多文档. 最后,一个阶段的工作告一段落,使用commit命令把暂存区的内容一股脑存到G ...

  9. MongoDB十二种最有效的模式设计【转】

    持续关注MongoDB博客(https://www.mongodb.com/blog)的同学一定会留意到,技术大牛Daniel Coupal 和 Ken W. Alger ,从 今年 2月17 号开始 ...

  10. Beyond Compare 3.3.8 build 16340 + Key

    本文摘录自冰点社区:http://forum.z27315.com/topic/14746-beyond-compare-338-build-16340-key/ Download Beyond Co ...