设计模式之备忘录模式| 8月更文挑战

149 阅读8分钟

在阎宏博士的《JAVA与模式》一书中开头是这样描述备忘录(Memento)模式的:备忘录模式又叫做快照模式(Snapshot Pattern)或Token模式,是对象的行为模式。备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉(Capture)住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。备忘录模式常常与命令模式和迭代子模式一同使用。

备忘录模式主要意图是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便在合适的时机将该对象恢复到原先保存的状态。结合起来理解,有三层含义:

  1. 不破坏封装性:对象只释放该暴露的接口,不能暴露不该对外释放的接口;
  2. 捕获对象内部状态并外部化:保存对象的状态,并外部化存储起来,以便进行恢复;
  3. 对象内部状态通过备忘录对象存储,保存在外部管理者类中。

备忘录模式.png

备忘录模式的核心角色有:备忘录角色(Memento)、发起人角色(Originator)、负责人角色(Caretaker)。

备忘录角色 备忘录角色用来存储发起人对象的内部状态,但是具体存储哪些字段值有发起人角色决定。备忘录对象的内部数据只能有发起人对象来访问,其它对象不应该访问到备忘录对象的内部数据。概括起来,备忘录角色的责任如下:

  1. 将发起人角色的内部状态进行存储,并进行外部化存储;
  2. 备忘录可以保护其内容不被发起人(Originator)对象之外的任何对象所读取,所以通常会把备忘录对象作为发起人对象的内部类来实现,而且实现成私有的,然后通常一个窄接口来标识对象的类型,以便以外部交互。

发起人角色 发起人角色也称之为原发器。发起人角色通过备忘录对象来存储某个时刻自身的状态,同时也可以使用备忘录保存的状态进行恢复。发起人角色用途有两个:

  1. 提供捕获某个时刻的方法,在方法中创建备忘录对象,把需要保存的状态进行保存,然后把备忘录对象外抛管理;
  2. 提供通过备忘录对象进行状态恢复的方法;

负责人角色 主要负责备忘录对象的管理。这里我们需要明确以下几点:

  1. 备忘录模式中并不一定需要一个负责人对象。广义来说,调用发起人角色获得备忘录对象后,备忘录放在哪里,那个对象就是管理者对象。
  2. 负责人对象并不是只管理一个备忘录对象,它可以管理多个备忘录对象。
  3. 狭义的负责人只管理同一类的备忘录对象,但广义的管理者可以管理不同类型的备忘录对象。
  4. 负责人对象需要实现的基本功能是:存入备忘录对象和从中获取备忘录对象。从功能上看,就是一个缓存功能或一个简单的对象实例池。
  5. 负责人角色虽然能存取备忘录对象,但是不能访问备忘录对象的内部数据。

通俗点说,备忘录就是一个普通类用来保存发起人角色的相关状态,然后将该状态交给负责人角色进行管理,这里的管理具有保存和恢复功能。

案例演示

这里就从魔兽世界的例子来说。刚学习玩魔兽世界的时候,先学习人机对战,玩到正起兴,室友突然喊你去吃饭,那只能先保存下进度,关电脑降降温,吃过饭回来直接打开刚才保存的进度,读取完成后接着玩。在这个案例中,就是一个备忘录模式的案例。

创建发起人角色/原发器角色

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. 简化了原发器,备忘录对象被保存在原发器对象之外,让客户来管理他们请求的状态,从而让原发器对象得到简化。

缺点

频繁地创建备忘录对象,可能导致较大的开销

相关模式

(1)备忘录模式和命令模式

命令模式实现中,在实现命令的撤销和重做的时候,可以使用备忘录模式,在命令操作的时候记录下操作前后的状态,然后在命令撤销和重做的时候,直接使用相应的备忘录对象来恢复状态就可以了。

(2)备忘录模式和原型模式

创建备忘录对象时,如果原发器对象中全部或大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例。