备忘录模式(Memento Pattern)属于行为型模式的一种,在不破坏封装特性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样就可以将该对象恢复到原先保存的状态。
概述
备忘录模式又叫做快照模式(Snapshot Pattern),一个用来存储另外一个对象内部状态的快照的对象。
备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉(Capture)住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。备忘录模式常常与命令模式和迭代子模式一同使用。
案例
前言:备忘录模式按照备忘录角色的形态不同,分为白箱实现与黑箱实现,两种模式与备忘录角色提供的接口模式有关;
引入两个定义,备忘录有两个等效的接口:
- 窄接口:负责人(Caretaker)对象(和其他除发起人对象之外的任何对象)看到的是备忘录的窄接口(narrow interface),这个窄接口只允许它把备忘录对象传给其他的对象。
- 宽接口:与负责人对象看到的窄接口相反的是,发起人对象可以看到一个宽接口(wide interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
白箱实现
角色组成
- Memento(备忘录角色): 负责存储原发器对象的内部状态,但是具体需要存储哪些数据是由原发器对象来决定的,在需要的时候提供原发器需要的内部状态。PS:这里可以存储状态。
- Originator(发起人(原发器)角色): 记录当前时刻的内部状态,负责定义哪些属于备份范围的状态,负责创建和恢复备忘录数据。
- Caretaker(备忘录负责人(管理者)角色): 对备忘录对象进行管理,但是不能对备忘录对象的内容进行操作或检查。
备忘录角色对任何对象都提供公共的访问,内部所存储的状态对对象公开,即为白箱实现。白箱实现发起人和负责人提供相同接口,使得负责人可以访问备忘录全部内容,并不安全。白箱实现对备忘录内容的保护靠的是程序员的自律,实现也很简单。
UML结构图
1.创建一个备忘录角色备忘录,内部定义了一个变量用来区分当前对象状态
public class Memento { private String state; public Memento(String state) { this.state = state; } public String getState() { return this.state; } public void setState(String state) { this.state = state; } }
2.接着创建一个原发器对象Originator,定义了创建备忘录对象和回滚的对象
public class Originator { private String state; public String getState() { return this.state; } public void setState(String state) { this.state = state; } /** * 创建对象 * * @return 备忘录对象 */ public Memento createMemento() { return new Memento(state); } /** * 从备忘录中恢复 * * @param memento 恢复的对象 */ public void restoreMemento(Memento memento) { this.state = memento.getState(); } }
3.创建备忘录管理者Caretaker,顾名思义就是来管理备忘录对象的
public class Caretaker { /** * 备忘录对象 */ private Memento memento; /** * 获取备忘录 * * @return 备忘录对象 */ public Memento retrieveMemento() { return this.memento; } /** * 存储备忘录对象 */ public void saveMemento(Memento memento) { this.memento = memento; } }
4.创建测试类
public class Client { public static void main(String[] args) { Originator originator = new Originator(); Caretaker caretaker = new Caretaker(); originator.setState("状态A"); System.out.println("当前状态:" + originator.getState()); // 存储内部状态 caretaker.saveMemento(originator.createMemento()); System.out.println("存档"); // 改变状态 originator.setState("状态B"); System.out.println("当前状态:" + originator.getState()); // 改变状态 originator.setState("状态C"); System.out.println("当前状态:" + originator.getState()); // 恢复状态 originator.restoreMemento(caretaker.retrieveMemento()); System.out.println("读档"); System.out.println("恢复后状态:" + originator.getState()); } }
5.运行结果
当前状态:状态A 存档 当前状态:状态B 当前状态:状态C 读档 恢复后状态:状态A
黑箱实现
角色组成
- MementoIF(备忘录角色): 空接口,不作任何实现。
- Originator(发起人(原发器)角色): 记录当前时刻的内部状态,负责定义哪些属于备份范围的状态,负责创建和恢复备忘录数据。这里Memento做为原发器的私有内部类,来存储备忘录。备忘录只能由原发器对象来访问它内部的数据,原发器外部的对象不应该能访问到备忘录对象的内部数据。
- Caretaker(备忘录负责人(管理者)角色): 对备忘录对象进行管理,但是不能对备忘录对象的内容进行操作或检查。
Memento 对象给 Originator 角色对象提供一个宽接口,而为其他对象提供一个窄接口,即为黑箱实现。
UML结构图
1.备忘录窄接口
public interface MementoIF { }
2.接着创建一个原发器对象Originator,其中定义了一个私有化的内部类Memento实现了MementoIF接口,只有当前对象能访问
public class Originator { private String state; public String getState() { return state; } public void setState(String state) { this.state = 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; } } }
3.创建备忘录管理者Caretaker,管理备忘录对象的
public class Caretaker { /** * 备忘录对象 */ private MementoIF memento; /** * 获取备忘录对象 */ public MementoIF retrieveMemento() { return memento; } /** * 保存备忘录对象 */ public void saveMemento(MementoIF memento) { this.memento = memento; } }
4.创建测试类
public class Client { public static void main(String[] args) { Originator originator = new Originator(); Caretaker caretaker = new Caretaker(); originator.setState("状态A"); System.out.println("当前状态:" + originator.getState()); // 改变状态 originator.setState("状态B"); System.out.println("当前状态:" + originator.getState()); // 存储内部状态 caretaker.saveMemento(originator.createMemento()); System.out.println("存档"); // 改变状态 originator.setState("状态C"); System.out.println("当前状态:" + originator.getState()); // 恢复状态 originator.restoreMemento(caretaker.retrieveMemento()); System.out.println("读档"); System.out.println("恢复后状态:" + originator.getState()); } }
5.运行结果
当前状态:状态A 当前状态:状态B 存档 当前状态:状态C 读档 恢复后状态:状态B
两者的代码结构是比较类似的,本质区别就是外部能不能访问备忘录的状态,备忘录角色具有安全等级;这里关于备忘录角色 -> 白箱实现利用的宽接口,黑箱模式利用的窄接口;
多重检查点
前面所给出的白箱和黑箱的示意性实现都是只存储一个状态的简单实现,也可以叫做只有一个检查点。常见的系统往往需要存储不止一个状态,而是需要存储多个状态,或者叫做有多个检查点。 这种情况只需要使用有序队列方式可以很容易达到多重检查点
备忘录模式可以将发起人对象的状态存储到备忘录对象里面,备忘录模式可以将发起人对象恢复到备忘录对象所存储的某一个检查点上。
自述历史
所谓自述历史模式(History-On-Self Pattern)实际上就是备忘录模式的一个变种。在备忘录模式中,发起人(Originator)角色、负责人(Caretaker)角色和备忘录 (Memento)角色都是独立的角色。虽然在实现上备忘录类可以成为发起人类的内部成员类,但是备忘录类仍然保持作为一个角色的独立意义。在自述历史模式里面,发起人角色自己兼任负责人角色。
完整代码在GIT项目中
总结
关于使用备忘录的潜在代价:
标准的备忘录模式的实现机制是依靠缓存来实现的,因此,当需要备忘的数据量较大时,或者是存储的备忘录对象数据量不大但是数量很多的时候,或者是用户很频繁的创建备忘录对象的时候,这些都会导致非常大的开销。
因此在使用备忘录模式的时候,一定要好好思考应用的环境,如果使用的代价太高,就不要选用备忘录模式,可以采用其它的替代方案。
关于增量存储:
如果需要频繁的创建备忘录对象,而且创建和应用备忘录对象来恢复状态的顺序是可控的,那么可以让备忘录进行增量存储,也就是备忘录可以仅仅存储原发器内部相对于上一次存储状态后的增量改变。
比如:在命令模式实现可撤销命令的实现中,就可以使用备忘录来保存每个命令对应的状态,然后在撤销命令的时候,使用备忘录来恢复这些状态。由于命令的历史列表是按照命令操作的顺序来存放的,也是按照这个历史列表来进行取消和重做的,因此顺序是可控的。那么这种情况,还可以让备忘录对象只存储一个命令所产生的增量改变而不是它所影响的每一个对象的完整状态。
优点
- 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
- 实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点
- 消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
- 由于备份的信息是由发起人自己提供的,所以管理者无法预知备份的信息的大小,所以在客户端使用时,可能一个操作占用了很大的内存,但客户端并不知晓。
适用场景
- 需要保存/恢复数据的相关状态场景。
- 提供一个可回滚的操作。
备忘录模式在很多软件的使用过程中普遍存在,但是在应用软件开发中,它的使用频率并不太高;