情景引入:
傍晚夕阳的玫瑰色的余晖铺满了大地,一切都是那么美好。大雄一边吹着口哨,一边迈着轻快的步伐背着书包进了家门。原来,这次大雄的数学破天荒考了个75分(要知道,以前他是经常不及格的)。为了表扬他,大雄妈妈也满足了他的一个小心愿,买了一个全新的掌上游戏机送给了他,也不忘叮嘱他要再接再厉,不要沉迷游戏,记得劳逸结合。高兴的大雄拿起游戏机马上玩了一会,然后就往空地跑,一溜烟就不见了人影。。。
原来,他打算来空地炫耀一番。但是,他刚到空地,就看到一群人围着小夫,嘴上还叽里咕噜说着什么。大雄放慢了脚步,带着好奇心悄悄地朝他们走了过去。原来,是小夫又靠他爸爸搞到了电视上刚刚推出的全新的高级掌上游戏机。这款游戏机也不一般,不仅能够对游戏关卡进行存档,而且能够恢复至任意一个之前打过的关卡(该关卡的状态包含了该关卡前面的所有关卡的状态)再继续打。大雄望了望小夫手里正在操纵着的高级游戏机,再瞥了一下自己的只能恢复到上一关卡的游戏机,叹了口气,低着头,悄悄地走开了。此时,夕阳的余晖也差不多消失殆尽,整个街道笼罩上了一层特殊的色彩。。。
一、简介
- 备忘录模式(Memento Pattern),又称为快照模式(Snapshot Pattern)或Token模式,属于行为型模式。
- 在我们日常生活中的例子俯拾皆是:打Dota的存档、影视剧中的“后悔药”、下棋时双方的悔棋、编写Word时常按的Ctrl+Z、数据库事务中的回滚操作等等。
- 很多时候,我们总是需要记录一个对象的内部状态,这样使得允许用户取消不确定或错误的操作,并能够恢复到他原先的状态,使得其有“后悔药”可以吃。
- 对于相对频繁而又简单的恢复/撤销操作并不需要存在磁盘中,只需要将保存在内存中的状态恢复一下即可,此时便可使用该模式。
二、具体内容
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
三、结构组成
其中客户端不与备忘录类耦合,而与备忘录的管理者类耦合。
Originator(发起人类/角色)
负责创建一个备忘录Memento,用以保存当前时刻自身的某些内部状态,并可使用备忘录恢复自身的内部状态。它可以根据需要决定Memento存储其自身的哪些内部状态。Memento(备忘录类/角色)
负责存储Originator对象的内部状态,并可防止Originator以外的其他对象访问备忘录Memento。它有两个接口,分别为宽接口和窄接口。其中,Caretaker管理者只能看到备忘录的窄接口(narrow interface),它只能将备忘录传递给其他对象,而无法访问备忘录的内容;Originator发起人能看到备忘录的宽接口(wide interface),允许它访问备忘录中返回到先前状态所需的所有数据。Caretaker(管理者类/角色)
负责保存好备忘录Memento,不能对备忘录中的内容进行操作或检查。
四、UML类图
1、”白箱”备忘录模式
在这种模式中,备忘录类对任何对象都提供一个宽接口,即备忘录类的内部所存储的状态对所有对象都公开,故称之为”白箱实现”。”白箱实现”中,将发起人类的状态存储在一个大家都看得到的地方,因此是破坏封装性的。但是,程序员们通过自律,也是能在一定程度上实现该模式的大部分用意。因此”白箱实现”还是有意义的。
2、”黑箱”备忘录模式
在这种模式中, 备忘录类对发起人类对象提供一个宽接口,而对其他对象(包括Caretaker类)提供一个窄接口, 称之为”黑箱实现”。在Java中,要实现双重接口,可以将备忘录类设计成为发起人类的内部成员类。”黑箱实现”中,将Memento类设置为Originator类的内部类,将Memento类(对象)封装在Originator里面,并在外部提供一个标识接口MementoIF(通常不含任何方法)给Caretaker以及其他对象。这样一来, Originator类看到的是Memento类的所有接口,而Caretaker类以及其他对象看到的仅仅是标识接口MementoIF所暴露出来的接口。
3、具有多重检查点的备忘录模式
前面给出的”白箱”和”黑箱”实现都只是保存一个状态(即一个检查点)的简单实现。而在这种模式中,系统可以保存多个检查点(多个状态)。具体来说,就是可以将发起人类对象的状态存储到备忘录对象里面,以后可以将发起人类对象恢复到备忘录对象所存储的某一个检查点上。
4、”自述历史”模式(History-On-Self Pattern)
是备忘录模式的一个变种。在这种模式中,发起人角色兼任管理者角色。
五、情景例子的实现代码
将上面的情境用代码实现一番:
1、”白箱”备忘录模式
//发起人类(普通掌上游戏机)
public class CommonPSP
{
//关卡的状态(包括多个属性,如角色的攻击力、防御力...)
private String stateOfGuanQia;
//创建备忘录(将当前关卡存档)
public Archive createArchive()
{
return new Archive(stateOfGuanQia);
}
//恢复备忘录(通过存档回到上一关卡)
public void restoreArchive(Archive archive)
{
stateOfGuanQia = archive.getStateOfGuanQia();
}
public String getStateOfGuanQia()
{
return stateOfGuanQia;
}
public void setStateOfGuanQia(String stateOfGuanQia)
{
this.stateOfGuanQia = stateOfGuanQia;
}
}
//备忘录类(游戏存档)
public class Archive
{
//关卡的状态(包括多个属性,如角色的攻击力、防御力...)
private String stateOfGuanQia;
public Archive(String stateOfGuanQia)
{
this.stateOfGuanQia = stateOfGuanQia;
}
public String getStateOfGuanQia()
{
return stateOfGuanQia;
}
public void setStateOfGuanQia(String stateOfGuanQia)
{
this.stateOfGuanQia = stateOfGuanQia;
}
}
//管理者类(掌上游戏机中的存档管理器)
public class ArchiveManager
{
private Archive archive;
//保存游戏存档
public void saveArchive(Archive archive)
{
this.archive = archive;
}
//获取游戏存档
public Archive retrieveArchive()
{
return archive;
}
}
public class Client
{
public static void main(String[] args)
{
CommonPSP commonPSP = new CommonPSP();
ArchiveManager archiveManager = new ArchiveManager();
//当前关卡的状态
commonPSP.setStateOfGuanQia("攻击力:80,防御力:100");
//创建游戏存档,将当前关卡的状态保存到存档中,并交由存档管理器管理
archiveManager.saveArchive(commonPSP.createArchive());
//新的关卡的状态
commonPSP.setStateOfGuanQia("攻击力:20,防御力:30");
//通过存档管理器中的存档恢复到上一个关卡的状态
commonPSP.restoreArchive(archiveManager.retrieveArchive());
System.out.println("当前状态为:" + commonPSP.getStateOfGuanQia());
}
}
输出结果:
当前状态为:攻击力:80,防御力:100
2、”黑箱”备忘录模式
//发起人类(普通掌上游戏机)
public class CommonPSP
{
//关卡的状态(包括多个属性,如角色的攻击力、防御力...)
private String stateOfGuanQia;
//创建备忘录(将当前关卡存档)
public ArchiveIF createArchive()
{
return new Archive(stateOfGuanQia);
}
//恢复备忘录(通过存档回到上一关卡)
public void restoreArchive(ArchiveIF archive)
{
stateOfGuanQia = ((Archive)archive).getStateOfGuanQia();
}
public String getStateOfGuanQia()
{
return stateOfGuanQia;
}
public void setStateOfGuanQia(String stateOfGuanQia)
{
this.stateOfGuanQia = stateOfGuanQia;
}
//备忘录类(游戏存档)
private class Archive implements ArchiveIF
{
//关卡的状态(包括多个属性,如角色的攻击力、防御力...)
private String stateOfGuanQia;
public Archive(String stateOfGuanQia)
{
this.stateOfGuanQia = stateOfGuanQia;
}
public String getStateOfGuanQia()
{
return stateOfGuanQia;
}
public void setStateOfGuanQia(String stateOfGuanQia)
{
this.stateOfGuanQia = stateOfGuanQia;
}
}
}
//窄接口,一个标识接口,没有定义任何方法
public interface ArchiveIF
{
}
//管理者类(掌上游戏机中的存档管理器)
public class ArchiveManager
{
//由于存档管理器拿到的是窄接口,故不可能改变游戏存档对象的内容
private ArchiveIF archive;
//保存游戏存档
public void saveArchive(ArchiveIF archive)
{
this.archive = archive;
}
//获取游戏存档
public ArchiveIF retrieveArchive()
{
return archive;
}
}
public class Client
{
public static void main(String[] args)
{
CommonPSP commonPSP = new CommonPSP();
ArchiveManager archiveManager = new ArchiveManager();
//当前关卡的状态
commonPSP.setStateOfGuanQia("攻击力:80,防御力:100");
//创建游戏存档,将当前关卡的状态保存到存档中,并交由存档管理器管理
archiveManager.saveArchive(commonPSP.createArchive());
//新的关卡的状态
commonPSP.setStateOfGuanQia("攻击力:20,防御力:30");
//通过存档管理器中的存档恢复到上一个关卡的状态
commonPSP.restoreArchive(archiveManager.retrieveArchive());
System.out.println("当前状态为:" + commonPSP.getStateOfGuanQia());
}
}
输出结果:
当前状态为:攻击力:80,防御力:100
3、具有多重检查点的备忘录模式
import java.util.ArrayList;
import java.util.List;
//发起人类(高级掌上游戏机)
public class AdvancedPSP
{
//当前关卡的状态(该关卡的状态由其前的所有关卡状态决定)
private List<String> statesOfGuanQias;
//当前关卡的索引
private int index;
public AdvancedPSP()
{
statesOfGuanQias = new ArrayList<>();
index = 0;
}
//将当前关卡存档(创建备忘录)
public Archive createArchive()
{
return new Archive(statesOfGuanQias, index);
}
//通过存档回到特定关卡(恢复备忘录)
public void restoreArchive(Archive archive)
{
statesOfGuanQias = archive.getStatesOfGuanQias();
index = archive.getIndex();
}
public void setStateOfGuanQia(String stateOfGuanQia)
{
statesOfGuanQias.add(stateOfGuanQia);
index++;
}
//输出当前关卡的状态(该关卡的状态由其前的所有关卡状态决定)
public void printStatesOfGuanQias()
{
for(String stateOfGuanQia: statesOfGuanQias)
{
System.out.print(stateOfGuanQia + " ");
}
}
}
import java.util.ArrayList;
import java.util.List;
//备忘录类(游戏存档,一个游戏存档对应一个游戏关卡的状态,而关卡的状态由其前的所有关卡状态决定))
public class Archive
{
//关卡的状态
private List<String> statesOfGuanQias;
//关卡的索引
private int index;
public Archive(List<String> statesOfGuanQias, int index)
{
this.statesOfGuanQias = new ArrayList<String>(statesOfGuanQias);
this.index = index;
}
//获取当前存档的关卡的状态(该关卡的状态由其前的所有关卡状态决定)
public List<String> getStatesOfGuanQias()
{
return statesOfGuanQias;
}
public int getIndex()
{
return index;
}
}
import java.util.ArrayList;
import java.util.List;
//管理者类(掌上游戏机中的存档管理器)
public class ArchiveManager
{
private AdvancedPSP advancedPSP;
private List<Archive> archives;
//当前关卡(当前检查点)
private int curIndex;
public ArchiveManager(AdvancedPSP advancedPSP)
{
this.advancedPSP = advancedPSP;
this.archives = new ArrayList<>();
this.curIndex = 0;
}
//创建一个游戏存档(创建一个检查点)
public int createArchive()
{
Archive archive = advancedPSP.createArchive();
archives.add(archive);
return curIndex++;
}
//恢复到某个关卡(恢复到某个检查点)
public void restoreArchive(int index)
{
Archive archive = archives.get(index);
advancedPSP.restoreArchive(archive);
}
//将某个特定关卡的存档删除(删除某个检查点)
public void removeArchive(int index)
{
archives.remove(index);
}
}
public class Client
{
public static void main(String[] args)
{
AdvancedPSP advancedPSP = new AdvancedPSP();
ArchiveManager archiveManager = new ArchiveManager(advancedPSP);
//当前关卡(关卡0)的状态
advancedPSP.setStateOfGuanQia("攻击力:10,防御力:20");
//创建游戏存档(建立一个检查点0)
archiveManager.createArchive();
//新的关卡(关卡1)的状态
advancedPSP.setStateOfGuanQia("攻击力:20,防御力:30");
//创建游戏存档(建立一个检查点1)
archiveManager.createArchive();
//新的关卡(关卡2)的状态
advancedPSP.setStateOfGuanQia("攻击力:30,防御力:40");
//创建游戏存档(建立一个检查点2)
archiveManager.createArchive();
//输出当前关卡的状态(该关卡的状态由其前的所有关卡状态决定)
advancedPSP.printStatesOfGuanQias();
//通过存档管理器中的存档恢复到关卡1的状态(恢复到检查点1)
archiveManager.restoreArchive(1);
System.out.println();
//输出当前关卡的状态(该关卡的状态由其前的所有关卡状态决定)
advancedPSP.printStatesOfGuanQias();
}
}
输出结果:
攻击力:10,防御力:20 攻击力:20,防御力:30 攻击力:30,防御力:40
攻击力:10,防御力:20 攻击力:20,防御力:30
4、”自述历史”模式(History-On-Self Pattern)
//窄接口,一个标识接口,没有定义任何方法
public interface ArchiveIF
{
}
//发起人类(普通掌上游戏机)兼任管理者类,负责保存自己的备忘录对象(游戏存档)
public class CommonPSP
{
//关卡的状态(包括多个属性,如角色的攻击力、防御力...)
private String stateOfGuanQia;
//改变关卡的状态
public void changeStateOfGuanQia(String stateOfGuanQia)
{
this.stateOfGuanQia = stateOfGuanQia;
}
//创建备忘录(将当前关卡存档)
public Archive createArchive()
{
return new Archive(this);
}
//恢复备忘录(通过存档回到上一关卡)
public void restoreArchive(ArchiveIF archive)
{
changeStateOfGuanQia(((Archive)archive).stateOfGuanQia);
}
public String getStateOfGuanQia()
{
return stateOfGuanQia;
}
//备忘录类(游戏存档)
private class Archive implements ArchiveIF
{
//关卡的状态(包括多个属性,如角色的攻击力、防御力...)
private String stateOfGuanQia;
public Archive(CommonPSP commonPSP)
{
this.stateOfGuanQia = commonPSP.stateOfGuanQia;
}
public String getStateOfGuanQia()
{
return stateOfGuanQia;
}
}
}
public class Client
{
public static void main(String[] args)
{
CommonPSP commonPSP = new CommonPSP();
//当前关卡的状态
commonPSP.changeStateOfGuanQia("攻击力:80,防御力:100");
//创建游戏存档
ArchiveIF archive = commonPSP.createArchive();
//新的关卡的状态
commonPSP.changeStateOfGuanQia("攻击力:20,防御力:30");
//通过存档恢复到上一个关卡的状态
commonPSP.restoreArchive(archive);
System.out.println("当前状态为:" + commonPSP.getStateOfGuanQia());
}
}
输出结果:
当前状态为:攻击力:80,防御力:100
六、优点
- 提供了一种可以恢复状态的机制。使用户可以方便地回到之前的某个状态。
- 实现了信息的封装。有时一些对象的内部信息必须保存在对象之外的地方,但是必须要由该对象自身来读取,此时,使用备忘录就可以把复杂的对象内部信息对其他的对象屏蔽起来,从而恰当地保持了封装地边界,使得用户/客户端不需要关心对象内部状态的保存细节(怎么保存这些状态,客户端不用知道)。
七、缺点
- 状态数据很耗内存资源。如果需要保存的类的成员变量(状态数据)过多,会占据很大的内存。 如果客户非常频繁地创建备忘录和恢复源发器状态,可能会导致非常大的开销。
- 操作开销大。如果客户端非常频繁地创建备忘录和恢复对象的内部状态,可能会导致非常大的开销。
八、什么时候用
- 需要保存/恢复数据的相关状态场景。
- 用于功能比较复杂的,但需要维护或记录属性历史的类,或者需要保存的属性只是众多属性中的一小部分。
- 如果在某个系统中使用命令模式时,需要实现命令的撤销功能,那么命令模式可以使用备忘录模式来存储可撤销操作的状态。
- 需要提供一个可回滚的操作时。
九、其他想说的
- 管理者类的存在就是为了使得备忘录模式符合迪米特原则。
- 为了节约内存,可以考虑使用原型模式+备忘录模式。