ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 引入 在阎宏博士的《JAVA与模式》一书中开头是这样描述备忘录(Memento)模式的: > 备忘录模式又叫做快照模式(Snapshot Pattern)或Token模式,是对象的行为模式。 备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉(Capture)住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。备忘录模式常常与命令模式和迭代子模式一同使用。 ## 定义 所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。 备忘录模式将要保存的细节给封装在备忘录中,就是那天要改变保存的细节也不会影响到客户端。 ## 结构 备忘录模式的结构图如下所示 ![](https://box.kancloud.cn/86476567df7e2878283f1323e3807555_461x180.png) 备忘录模式所涉及的角色有三个:备忘录(Memento)角色、发起人(Originator)角色、负责人(Caretaker)角色。 **备忘录(Memento)角色**   备忘录角色又如下责任: * (1)将发起人(Originator)对象的内战状态存储起来。备忘录可以根据发起人对象的判断来决定存储多少发起人(Originator)对象的内部状态。 * (2)备忘录可以保护其内容不被发起人(Originator)对象之外的任何对象所读取。 备忘录有两个等效的接口: * 窄接口:负责人(Caretaker)对象(和其他除发起人对象之外的任何对象)看到的是备忘录的窄接口(narrow interface),这个窄接口只允许它把备忘录对象传给其他的对象。 * 宽接口:与负责人对象看到的窄接口相反的是,发起人对象可以看到一个宽接口(wide interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。 **发起人(Originator)角色** 发起人角色有如下责任: * (1)创建一个含有当前的内部状态的备忘录对象。 * (2)使用备忘录对象存储其内部状态。 **负责人(Caretaker)角色** 负责人角色有如下责任: * (1)负责保存备忘录对象。 * (2)不检查备忘录对象的内容。 ## 代码实现 小时候都有过打游戏的经历,在一个小游戏中,每过一关都可以存档,当挑战下一关没有通过时,可以直接通过读档这一操作,从通过的最近一关开始过起。而不是从第一关重新再打。现在通过备忘录模式,演示这个场景。 ### “白箱”备忘录模式的实现   备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。因此这个实现又叫做“白箱实现”。 “白箱”实现将发起人角色的状态存储在一个大家都看得到的地方,因此是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意。因此白箱实现仍然是有意义的。 下面给出一个示意性的“白箱实现”。 原生器类(发起者) ``` public class Originator { private long missionNumber; //添加备份 public Memento createMemento(){ return new Memento(this.missionNumber); } //恢复备份 public void restoreMemento(Memento memento){ setMissionNumber(memento.getMissionNumber()); } public long getMissionNumber() { return missionNumber; } public void setMissionNumber(long missionNumber) { this.missionNumber = missionNumber; } } ``` 备忘录类 ``` public class Memento { //记录游戏的关卡数 private long missionNumber; public Memento(long missionNumber){ this.missionNumber = missionNumber; } public long getMissionNumber() { return missionNumber; } public void setMissionNumber(long missionNumber) { this.missionNumber = missionNumber; } } ``` 管理者类 ``` public class Caretaker { //备忘录 private Memento memento; public Memento getMemento() { return memento; } public void setMemento(Memento memento) { this.memento = memento; } } ``` 测试类 ``` public class MementoTest { public static void main(String[] args) { Originator originator = new Originator(); originator.setMissionNumber(99); System.out.println("现在正在过游戏的99关"); Caretaker caretaker = new Caretaker(); caretaker.setMemento(originator.createMemento()); System.out.println("成功过关,开始存档"); originator.setMissionNumber(100); System.out.println("现在正在过游戏的100关"); System.out.println("很遗憾闯关失败,启动备忘录模式,回到最近通过的那一关"); originator.restoreMemento(caretaker.getMemento()); System.out.println("现在关数: "+originator.getMissionNumber()); } } ``` 输出: ![](https://box.kancloud.cn/ea68a8c37c21d7570a954cd7be29f45b_453x139.png) ### “黑箱”备忘录模式的实现   备忘录角色对发起人(Originator)角色对象提供一个宽接口,而为其他对象提供一个窄接口。这样的实现叫做“黑箱实现”。 在JAVA语言中,实现双重接口的办法就是将备忘录角色类设计成发起人角色类的内部成员类。 将Memento设成Originator类的内部类,从而将Memento对象封装在Originator里面;在外部提供一个标识接口MementoIF给Caretaker以及其他对象。这样,Originator类看到的是Menmento的所有接口,而Caretaker以及其他对象看到的仅仅是标识接口MementoIF所暴露出来的接口。 发起人角色类Originator中定义了一个内部的Memento类。由于此Memento类的全部接口都是私有的,因此只有它自己和发起人类可以调用。 ``` public class Originator2 { private long missionNumber; //添加备份 public MementoIF createMemento() { return new Memento(this.missionNumber); } //恢复备份 public void restoreMemento(MementoIF memento) { setMissionNumber(((Memento)memento).getMissionNumber()); } public long getMissionNumber() { return missionNumber; } public void setMissionNumber(long missionNumber) { this.missionNumber = missionNumber; } private class Memento implements MementoIF{ private long missionNumber; public Memento(long missionNumber){ this.missionNumber = missionNumber; } public long getMissionNumber() { return missionNumber; } public void setMissionNumber(long missionNumber) { this.missionNumber = missionNumber; } } } ``` 窄接口MementoIF,这是一个标识接口,因此它没有定义出任何的方法。 ``` public interface MementoIF { } ``` 负责人角色类Caretaker能够得到的备忘录对象是以MementoIF为接口的,由于这个接口仅仅是一个标识接口,因此负责人角色不可能改变这个备忘录对象的内容。 ``` public class Caretaker { //备忘录 private MementoIF memento; public MementoIF getMemento() { return memento; } public void setMemento(MementoIF memento) { this.memento = memento; } } ``` 客户端角色类 ``` public class MementoTest { public static void main(String[] args) { Originator2 originator = new Originator2(); originator.setMissionNumber(99); System.out.println("现在正在过游戏的99关"); Caretaker caretaker = new Caretaker(); caretaker.setMemento(originator.createMemento()); System.out.println("成功过关,开始存档"); originator.setMissionNumber(100); System.out.println("现在正在过游戏的100关"); System.out.println("很遗憾闯关失败,启动备忘录模式,回到最近通过的那一关"); originator.restoreMemento(caretaker.getMemento()); System.out.println("现在关数: "+originator.getMissionNumber()); } } ``` ### 多重检查点 前面所给出的白箱和黑箱的示意性实现都是只存储一个状态的简单实现,也可以叫做只有一个检查点。常见的系统往往需要存储不止一个状态,而是需要存储多个状态,或者叫做有多个检查点。 备忘录模式可以将发起人对象的状态存储到备忘录对象里面,备忘录模式可以将发起人对象恢复到备忘录对象所存储的某一个检查点上。下面给出一个示意性的、有多重检查点的备忘录模式的实现。 发起人角色源代码 ``` public class Originator3 { private List<String> names; private int index; public Originator3(){ names = new ArrayList<>(); index = 0; } //添加备份 public Memento createMemento() { return new Memento(names, index); } //恢复备份 public void restoreMemento(Memento memento) { names = memento.getNames(); index = memento.getIndex(); } public void setName(String name) { names.add(name); index++; } public void printNames(){ for (String name: names){ System.out.println(name); } } } ``` 备忘录角色类,这个实现可以存储任意多的状态,外界可以使用检查点指数index来取出检查点上的状态。 ``` public class Memento { //记录游戏的关卡数 private List<String> names; private int index; public List<String> getNames() { return names; } public int getIndex() { return index; } public Memento(List<String> names, int index) { this.names = new ArrayList<>(names);//这里必须要New this.index = index; } } ``` 负责人角色类 ``` public class Caretaker { //备忘录 private List<Memento> mementos = new ArrayList<>(); private Originator3 originator; private int current; public Caretaker(Originator3 originator) { this.originator = originator; current = 0; } /** * 创建一个新的检查点 * @return */ public int createMemento(){ Memento memento = originator.createMemento(); mementos.add(memento); return current++; } /** * 将发起人恢复到某个检查点 * @param index */ public void restoreMemento(int index){ Memento memento = mementos.get(index); originator.restoreMemento(memento); } public void removeMemento(int index){ mementos.remove(index); } } ``` ``` public class MementoTest { public static void main(String[] args) { Originator3 originator = new Originator3(); originator.setName("第一关"); System.out.println("现在正在过游戏的第一关"); Caretaker caretaker = new Caretaker(originator); caretaker.createMemento(); System.out.println("成功过关,开始存档"); originator.setName("第二关"); System.out.println("现在正在过游戏的第二关"); caretaker.createMemento(); System.out.println("成功过关,开始存档"); originator.setName("第三关"); System.out.println("现在正在过游戏的第三关"); caretaker.createMemento(); System.out.println("成功过关,开始存档"); originator.setName("第四关"); System.out.println("现在正在过游戏的第四关"); System.out.println("很遗憾闯关失败,当前记录为"); originator.printNames(); System.out.println("启用备份,回退"); caretaker.restoreMemento(1); originator.printNames(); } } ``` ### “自述历史”模式   所谓“自述历史”模式(History-On-Self Pattern)实际上就是备忘录模式的一个变种。在备忘录模式中,发起人(Originator)角色、负责人(Caretaker)角色和备忘录(Memento)角色都是独立的角色。虽然在实现上备忘录类可以成为发起人类的内部成员类,但是备忘录类仍然保持作为一个角色的独立意义。在“自述历史”模式里面,发起人角色自己兼任负责人角色。   “自述历史”模式的类图如下所示: ![](https://box.kancloud.cn/d851130061a35e094b581efacbc5fa67_410x314.png) 备忘录角色有如下责任: * (1)将发起人(Originator)对象的内部状态存储起来。 * (2)备忘录可以保护其内容不被发起人(Originator)对象之外的任何对象所读取。 发起人角色有如下责任: * (1)创建一个含有它当前的内部状态的备忘录对象。 * (2)使用备忘录对象存储其内部状态。   客户端角色有负责保存备忘录对象的责任。 发起人角色同时还兼任负责人角色,也就是说它自己负责保持自己的备忘录对象。 ``` public class Originator4 { private long missionNumber; //添加备份 public MementoIF createMemento() { return new Memento(this); } //恢复备份 public void restoreMemento(MementoIF memento) { setMissionNumber(((Memento) memento).getMissionNumber()); } public long getMissionNumber() { return missionNumber; } public void setMissionNumber(long missionNumber) { this.missionNumber = missionNumber; } private class Memento implements MementoIF { private long missionNumber; /** * 构造方法 */ private Memento(Originator4 o) { this.missionNumber = o.missionNumber; } private long getMissionNumber() { return missionNumber; } } } ``` 窄接口MementoIF,这是一个标识接口,因此它没有定义出任何的方法。 ``` public interface MementoIF { } ``` 客户端角色 ``` public class MementoTest { public static void main(String[] args) { Originator4 originator = new Originator4(); originator.setMissionNumber(99); System.out.println("现在正在过游戏的99关"); System.out.println("成功过关,开始存档"); MementoIF mementoIF = originator.createMemento();//存档99 originator.setMissionNumber(100); System.out.println("现在正在过游戏的100关"); System.out.println("很遗憾闯关失败,启动备忘录模式,回到最近通过的那一关"); originator.restoreMemento(mementoIF); System.out.println("现在关数: "+originator.getMissionNumber()); } } ``` 由于“自述历史”作为一个备忘录模式的特殊实现形式非常简单易懂,它可能是备忘录模式最为流行的实现形式。 ## 优点 * 有时一些发起人对象的内部信息必须保存在发起人对象以外的地方,但是必须由发起人对象自己读取。这时使用备忘录模式可以将复杂的发起人内部信息对其它对象屏蔽起来。从而可以恰当的保持封装的边界。 * 备忘录模式简化了发起者类。发起者不再需要管理和保存其内部状态的一个个版本。客户端可以自行管理他们所需要的这些状态的版本。 * 当发起者角色的状态发生改变的时候,有可能这个状态无效,需要回滚到前一个状态,这时候可以使用暂时存储起来的备忘录将状态进行还原。 ## 缺点 * 如果发起者角色的状态有很多并且需要完整的储存到备忘录对象中,那么备忘录对象会很消耗资源。 **注意事项** 备忘录模式最理想的情况是只允许生成该备忘录的那个原发器能够访问这个备忘录的内部状态。 ## 适用场景 * 1、 需要保存一个对象在某一个时刻的状态或部分状态。 * 2、 如果用一个接口来让其他对象得到这些状态,将会暴露对象的实现细节并破坏对象的封装性,一个对象不希望外界直接访问其内部状态,通过负责人可以间接访问其内部状态。 ## 总结 * 1、 备忘录模式可以实现在不破坏封装的前提下,捕获一个类的内部状态,并且在该对象之外保存该对象的状态,保证该对象能够恢复到历史的某个状态。 * 2、 备忘录模式实现了内部状态的封装,除了创建它的原发器之外其他对象都不能够访问它。 * 3、 备忘录模式会占用较多的内存,消耗资源。