备忘录模式 Memento 快照模式 标记Token模式 行为型 设计模式(二十二)
备忘录模式 Memento
沿着脚印,走过你来时的路,回到原点。
苦海翻起爱恨
在世间难逃避命运
相亲竟不可接近
或我应该相信是缘份
一首《一生所爱》触动了多少人的心弦,一段五百年都没有结果的爱情,让多少人潸然泪下。
有人说:当你真正看懂了《大话西游》,你就真的懂了爱情;那么,当你听懂了《一生所爱》,你就真的懂了什么是哀伤和无奈。
《大话西游》是不可超越的经典,在《月光宝盒》中,至尊宝为了救白晶晶,使用月光宝盒使时光倒流,几次后产生故障,竟将其带回五百年前,这时紫霞仙子(朱茵饰)向她走来......
百度百科中关于词条“VMware快照”的描述
磁盘“快照”是虚拟机磁盘文件(VMDK)在某个点即时的复本。
系统崩溃或系统异常,你可以通过使用恢复到快照来保持磁盘文件系统和系统存储。
当升级应用和服务器及给它们打补丁的时候,快照是救世主。VMware快照是VMware Workstation里的一个特色功能。
“脚印”记录了我们来时的路,所以我们可以原路返回; “月光宝盒”记录了时间的状态,所以至尊宝可以时光穿梭; “VMware快照”记录了虚拟机磁盘文件(VMDK)在某个点的状态,所以可以通过快照进行系统恢复; 以上,他们都记录了状态,进而可以恢复到原来的状态
快照信息保存在备忘录Memento中
通过管理员CareTaker进行保管
“脚印”记录了我们来时的路,所以我们可以原路返回; “月光宝盒”记录了时间的状态,所以至尊宝可以时光穿梭; “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,不曾访问内部细节)
内部类方式重构
在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)为快照恢复 步骤解析:- 客户端程序对原发器Originator进行状态设置
- 客户端程序对原发器进行快照创建
- 客户端程序对快照进行保存
- 客户端程序获得快照
- 客户端程序根据快照进行状态恢复
重构小结
重构的逻辑为:- 借助于私有内部类实现了对外界的封闭
- 将负责人管理员CareTaker与Memento进行解耦,通过抽象MementoInterface进行连接
形式变换
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对象的状态,这与白盒方式的备忘录模式效果一样