1.前言
备忘录模式,又称快照模式,属于行为型模式。该种设计模式提供了一种对象状态恢复的机制,使得对象可以很方便的对历史状态进行回溯,恢复到特定某历史时刻的状态。
结合现实生活中的示例比较容易理解,比如文本编辑器的撤销机制,游戏存档机制,虚拟机的快照机制,都是很好的例子,以虚拟机的快照机制来说,通过对当前系统拍摄一系列快照(snapshot),记录当前的系统的状态,比如磁盘存储,内存,进程等信息,可以在未来需要回退的时候执行恢复快照操作,将系统“瞬间”变回快照制作时的状态。
而备忘录模式就是这类机制的原理和思想。
定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存该状态,以便以后当需要时能够将该对象恢复到原先保存的状态
先给出备忘录设计模式的角色:
- 原发器角色(Originator):主体业务对象,应该提供创建备忘录和恢复备忘录的功能,能够随时从备忘录对象中恢复自身的历史状态
- 备忘录角色(Memento):负责存储原发器的内部状态,在需要恢复的时候将这些状态提供给原发器对象
- 管理者角色(Caretaker):管理备忘录的角色,备忘录对象的存储位置,提供基本的保存/获取备忘录方法,但不能修改(备忘录可以修改,那就不叫备忘录了)
偷个UML:
图片来源:百度图库
从角色设计中,我们可以总结出一个设计模式的基本流程:
那就是将一个对象A的状态保存到另外一个对象B(这两个对象的结构理论上是相似的),然后在必要的时候根据B对象恢复A对象,且B对象应该不能被随意改变。
看定义中提到了,“不破坏封装性”,这个是备忘录模式中的关键点,如果备忘录模式中封装性得不到保证,那也无从谈起备忘录。
面向对象三大特性最重要的特性就是封装,即对对象的隐私数据进行封装,对外部不可见,或者只允许通过特定的方式进行有限访问。
现代编程中,一大堆裸奔的setter/getter方法,其实算不上封装,懒而已。
备忘录模式中的封装性指的就是作为备忘录对象的隐私数据是不能被外界访问到,这里的不能访问指的是在语言层面的约束,而非程序员自身的约束。
下面以一个示例来说明备忘录模式以及其封装性如何保证
2.示例
假设一个进程可以进行快照恢复,设执行过程中有N中状态,可以在任意状态对进程的状态保存快照,以便后期可以在需要的时候将其恢复至某一个历史状态,实现有两种方式,第一种是被称之为“宽接口”的方式,也叫白盒实现,如下
首先定义原发器,也就是需要备忘的对象,简单设定一个state字段,表示对象当前状态,三个方法(stage1,stage2,stage3)表示三种执行流程,其中最重要的是要提供保存快照和恢复快照的方法。
/**
* @description: 原发器对象
* @version: 1.0
*/
public class Process {
private int state;
public Process() {
this.state = 0;
}
public void initState(){
this.state = 0;
}
public void stage1() {
this.state = 1;
}
public void stage2() {
this.state = 2;
}
public void stage3() {
this.state = 3;
}
public Snapshot saveSnapshot(){
return new Snapshot(this.state);
}
public void recoverySnapshot(Snapshot snapshot){
this.state = snapshot.getState();
}
public void showState(){
System.out.println("当前进程状态:" + state);
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
接下来就是备忘录对象,备忘录对象目的是用于保存原发器对象的快照信息
/**
* @description: 备忘录对象
* @version: 1.0
*/
public class Snapshot {
private int state;
public Snapshot(int state) {
this.state = state;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
备忘录管理对象
/**
* @description: 备忘录管理对象
* @version: 1.0
*/
public class SnapshotCaretaker {
//这里用一个栈模拟 快照的存储
private Stack<Snapshot> snapshotStack;
public SnapshotCaretaker(){
snapshotStack = new Stack<>();
}
public Snapshot pop() {
return snapshotStack.pop();
}
public void push(Snapshot snapshot){
snapshotStack.push(snapshot);
}
}
测试:
/**
* @description: test
* @version: 1.0
*/
public class Test {
public static void main(String[] args) {
SnapshotCaretaker snapshotCaretaker = new SnapshotCaretaker();
Process process = new Process();
process.initState();
//逐步创建快照
process.stage1();
process.showState();
snapshotCaretaker.push(process.saveSnapshot());
process.stage2();
process.showState();
snapshotCaretaker.push(process.saveSnapshot());
process.stage3();
process.showState();
snapshotCaretaker.push(process.saveSnapshot());
//snapshotCaretaker.push(new Snapshot(666)); //error
//比如这里可以修改快照管理对象中的备忘录对象的,破坏了封装性,只能靠程序员去约束
System.out.println("==========恢复快照");
//恢复快照
process.recoverySnapshot(snapshotCaretaker.pop());
process.showState();
process.recoverySnapshot(snapshotCaretaker.pop());
process.showState();
process.recoverySnapshot(snapshotCaretaker.pop());
process.showState();
}
}
input:
当前进程状态:1
当前进程状态:2
当前进程状态:3
==========恢复快照
当前进程状态:3
当前进程状态:2
当前进程状态:1
这种方式就称之为宽接口,宽接口指的是对于外部来说,备忘录对象的操作方式是比较完备的,或者说操作的范围比较“宽”,比如这里的备忘录对象在外部随时都可以创建它,这会出问题的,只能通过程序员的道德去约束,但在语言层面是可以访问到的,算不上完整意义上的封装。
黑盒实现(窄接口):
通过将备忘录对象设计成原发器对象的内部类,并且声明为私有,对于原发器来说,可以访问所有成员,这里属于宽接口的方式。但是外部如何访问?答案是外部不需要访问,通过原发器方法创建该对象的实例是唯一的方式,除此之外再无其他地方能够创建它。
既然外部访问不到,那备忘录管理对象如何管理它?
答案就是通过声明一个空的接口,并让备忘录对象实现该接口,备忘录管理对象使用更抽象的接口类型管理它(备忘录对象是原发器对象创建的,具体是什么外部不需要知道,也不能创建)
/**
* @description: 对外提供一个空实现的窄接口,
* @version: 1.0
*/
public interface Memento {
}
将备忘录声明为原发器的内部类,并私有,只有原发器能够访问,是宽接口的方式。内部备忘录类要实现Memento 空接口,这里是作为一个窄接口对外提供。
窄接口和宽接口相反,能操作的方式不完备,比较“窄”
/**
* @description: 原发器对象
* @version: 1.0
*/
public class Process {
private int state;
public void initState(){
this.state = 0;
}
public void stage1() {
this.state = 1;
}
public void stage2() {
this.state = 2;
}
public void stage3() {
this.state = 3;
}
public Memento saveSnapshot(){
return new Snapshot(this.state);
}
public void recoveryState(Memento memento){
Snapshot snapshot = (Snapshot) memento;
this.state = snapshot.state;
}
public void showState(){
System.out.println("当前进程状态:" + state);
}
//私有内部类的形式维护备忘录对象
private class Snapshot implements Memento{
private int state;
public Snapshot(int state){
this.state = state;
}
private int getState() {
return state;
}
private void setState(int state) {
this.state = state;
}
}
}
备忘录管理对象,内部都是抽象表示
/**
* @description: 备忘录管理对象
* @version: 1.0
*/
public class SnapshotCaretaker {
private Stack<Memento> mementoStack;
public SnapshotCaretaker(){
mementoStack = new Stack<>();
}
public Memento pop() {
return mementoStack.pop();
}
public void push(Memento snapshot){
mementoStack.push(snapshot);
}
}
测试:
/**
* @description: test
* @version: 1.0
*/
public class Test {
public static void main(String[] args) {
SnapshotCaretaker snapshotCaretaker = new SnapshotCaretaker();
Process process = new Process();
process.initState();
//创建快照
process.stage1();
process.showState();
snapshotCaretaker.push(process.saveSnapshot());
process.stage2();
process.showState();
snapshotCaretaker.push(process.saveSnapshot());
process.stage3();
process.showState();
snapshotCaretaker.push(process.saveSnapshot());
//符合封装要求,process.saveSnapshot()是唯一的方式
System.out.println("==========恢复快照");
//恢复快照
process.recoveryState(snapshotCaretaker.pop());
process.showState();
process.recoveryState(snapshotCaretaker.pop());
process.showState();
process.recoveryState(snapshotCaretaker.pop());
process.showState();
}
}
input:
当前进程状态:1
当前进程状态:2
当前进程状态:3
==========恢复快照
当前进程状态:3
当前进程状态:2
当前进程状态:1
3.总结
备忘录设计模式也是第一次接触,虽然看上去比较简单,但是其中的封装性却比较重要,优点无非就是可以在任意时刻将对象状态恢复至某一个历史时刻状态,内部实现了封装性,使得外部对象无法改变。缺点则是如果快照保存比较频繁,对资源消耗会相对较大,尤其是大对象,需要保存的状态较多。