在阎宏博士的《JAVA与模式》一书中开头是这样描述备忘录(Memento)模式的:备忘录模式又叫做快照模式(Snapshot Pattern)或Token模式,是对象的行为模式。备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉(Capture)住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。备忘录模式常常与命令模式和迭代子模式一同使用。
备忘录模式主要意图是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便在合适的时机将该对象恢复到原先保存的状态。结合起来理解,有三层含义:
- 不破坏封装性:对象只释放该暴露的接口,不能暴露不该对外释放的接口;
- 捕获对象内部状态并外部化:保存对象的状态,并外部化存储起来,以便进行恢复;
- 对象内部状态通过备忘录对象存储,保存在外部管理者类中。
备忘录模式的核心角色有:备忘录角色(Memento)、发起人角色(Originator)、负责人角色(Caretaker)。
备忘录角色
备忘录角色用来存储发起人对象的内部状态,但是具体存储哪些字段值有发起人角色决定。备忘录对象的内部数据只能有发起人对象来访问,其它对象不应该访问到备忘录对象的内部数据。概括起来,备忘录角色的责任如下:
- 将发起人角色的内部状态进行存储,并进行外部化存储;
- 备忘录可以保护其内容不被发起人(Originator)对象之外的任何对象所读取,所以通常会把备忘录对象作为发起人对象的内部类来实现,而且实现成私有的,然后通常一个窄接口来标识对象的类型,以便以外部交互。
发起人角色
发起人角色也称之为原发器。发起人角色通过备忘录对象来存储某个时刻自身的状态,同时也可以使用备忘录保存的状态进行恢复。发起人角色用途有两个:
- 提供捕获某个时刻的方法,在方法中创建备忘录对象,把需要保存的状态进行保存,然后把备忘录对象外抛管理;
- 提供通过备忘录对象进行状态恢复的方法;
负责人角色
主要负责备忘录对象的管理。这里我们需要明确以下几点:
- 备忘录模式中并不一定需要一个负责人对象。广义来说,调用发起人角色获得备忘录对象后,备忘录放在哪里,那个对象就是管理者对象。
- 负责人对象并不是只管理一个备忘录对象,它可以管理多个备忘录对象。
- 狭义的负责人只管理同一类的备忘录对象,但广义的管理者可以管理不同类型的备忘录对象。
- 负责人对象需要实现的基本功能是:存入备忘录对象和从中获取备忘录对象。从功能上看,就是一个缓存功能或一个简单的对象实例池。
- 负责人角色虽然能存取备忘录对象,但是不能访问备忘录对象的内部数据。
通俗点说,备忘录就是一个普通类用来保存发起人角色的相关状态,然后将该状态交给负责人角色进行管理,这里的管理具有保存和恢复功能。
案例演示
这里就从魔兽世界的例子来说。刚学习玩魔兽世界的时候,先学习人机对战,玩到正起兴,室友突然喊你去吃饭,那只能先保存下进度,关电脑降降温,吃过饭回来直接打开刚才保存的进度,读取完成后接着玩。在这个案例中,就是一个备忘录模式的案例。
创建发起人角色/原发器角色
public class Dota {
/**
* 游戏开始时间
*/
private int time;
/**
* 游戏人头数
*/
private int killPeople;
/**
* 是否暂停
*/
private boolean isPause = false;
/**
* 玩游戏
*/
public void playGame(){
new Thread(new Runnable() {
@Override
public void run() {
while(!isPause){
System.out.println("游戏开始了:" + time + "分钟,人头数:" + killPeople);
time++;
killPeople++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
/**
* 结束游戏
*/
public void exitGame(){
isPause = true;
System.out.println("=====结束游戏=====");
System.out.println("游戏开始了:" + time + "分钟,人头数:" + killPeople);
System.out.println("===============");
}
/**
* 保存获取当前游戏信息
* @return
*/
public GameInfo saveGameInfo(){
return new GameInfo(time, killPeople);
}
/**
* 重新加载游戏
* @param gameInfo
*/
public void loadGame(GameInfo gameInfo){
time = gameInfo.getTime();
killPeople = gameInfo.getKillPeople();
System.out.println("=====恢复游戏=====");
System.out.println("游戏开始了:" + time + "分钟,人头数:" + killPeople);
System.out.println("===============");
isPause = false;
}
}
在原发器的定义中,定义了一个获取内部状态的方法,并将内部状态保存到备忘录对象中。同时定义了一个loadGame方法用来读取备忘录中的内容。
定义备忘录
/**
* 备忘录角色,一个特殊的类,用来存放原发器的信息
* @author Iflytek_dsw
*
*/
public class GameInfo {
private int time;
private int killPeople;
public GameInfo(int time, int killPeople) {
super();
this.time = time;
this.killPeople = killPeople;
}
public int getTime() {
return time;
}
public void setTime(int time) {
this.time = time;
}
public int getKillPeople() {
return killPeople;
}
public void setKillPeople(int killPeople) {
this.killPeople = killPeople;
}
}
可以看到,备忘录就是一个简单的实体类,这个实体类用来存放原发器的状态。这个类的内容只能被原发器访问,即原发器与备忘录对象之间建立的是一个宽接口。原发器能够看到一个宽接口,允许它访问所需的所有数据,来返回先前的状态。通常实现成为原发器内的一个私有内部类。
定义负责人角色/管理者角色
/**
* 负责人角色,充当备忘录模式管理角色
* @author Iflytek_dsw
*
*/
public class GameManager {
private Map<String, GameInfo> gameMap;
private static GameManager instance;
private GameManager(){
gameMap = new ConcurrentHashMap<>();
}
public static GameManager getGameManager(){
if(instance == null){
synchronized(GameManager.class){
if(instance == null){
instance = new GameManager();
}
}
}
return instance;
}
/**
* 保存游戏信息
* @param name
* @param gameInfo
*/
public void saveGameInfo(String name, GameInfo gameInfo){
gameMap.put(name, gameInfo);
}
/**
* 读取游戏信息
* @param name
* @return
*/
public GameInfo getGameInfo(String name){
return gameMap.get(name);
}
}
负责人角色用来管理备忘录对象,管理者对象并不是只管理一个备忘录对象,它可以管理多个备忘录对象。管理者只能看到备忘录的窄接口,这个接口的实现通常没有任何的方法,只是一个类型标识。窄接口使得管理者只能将备忘录传递给其他对象
客户端
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
Dota dota = new Dota();
dota.playGame();
try {
//玩了一会
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//暂停游戏
dota.exitGame();
GameManager.getGameManager().saveGameInfo("备忘录模式", dota.saveGameInfo());
//恢复游戏
dota.loadGame(GameManager.getGameManager().getGameInfo("备忘录模式"));
}
}
运行结果
游戏开始了:0分钟,人头数:0
游戏开始了:1分钟,人头数:1
游戏开始了:2分钟,人头数:2
游戏开始了:3分钟,人头数:3
游戏开始了:4分钟,人头数:4
游戏开始了:5分钟,人头数:5
游戏开始了:6分钟,人头数:6
游戏开始了:7分钟,人头数:7
游戏开始了:8分钟,人头数:8
游戏开始了:9分钟,人头数:9
游戏开始了:10分钟,人头数:10
游戏开始了:11分钟,人头数:11
游戏开始了:12分钟,人头数:12
游戏开始了:13分钟,人头数:13
游戏开始了:14分钟,人头数:14
游戏开始了:15分钟,人头数:15
游戏开始了:16分钟,人头数:16
游戏开始了:17分钟,人头数:17
游戏开始了:18分钟,人头数:18
游戏开始了:19分钟,人头数:19
=====结束游戏=====
游戏开始了:20分钟,人头数:20
===============
=====恢复游戏=====
游戏开始了:20分钟,人头数:20
===============
游戏开始了:20分钟,人头数:20
游戏开始了:21分钟,人头数:21
游戏开始了:22分钟,人头数:22
游戏开始了:23分钟,人头数:23
备忘录模式的优缺点
优点
- 更好的封装性,通过使用备忘录对象来封装原发器对象的内部状态,虽然这个对象保存在原发器对象的外部,但是由于备忘录对象的窄接口并不提供任何方法,因为有效地保证了原发器内部状态的封装,不把原发器对象的内部实现细节暴露给外部。
- 简化了原发器,备忘录对象被保存在原发器对象之外,让客户来管理他们请求的状态,从而让原发器对象得到简化。
缺点
频繁地创建备忘录对象,可能导致较大的开销
相关模式
(1)备忘录模式和命令模式
命令模式实现中,在实现命令的撤销和重做的时候,可以使用备忘录模式,在命令操作的时候记录下操作前后的状态,然后在命令撤销和重做的时候,直接使用相应的备忘录对象来恢复状态就可以了。
(2)备忘录模式和原型模式
创建备忘录对象时,如果原发器对象中全部或大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例。