【笔记整理】图解设计模式 | 第18章 Memento模式(保存状态对象)

【笔记整理】图解设计模式 | 导航


定义

  • 通过引入表示实例状态的角色,可以在保存和恢复实例时有效地防止对象的封装性遭到破坏。
  • 事先将某个时间点的实例的状态保存下来,之后在有必要时,再将实例恢复至当时的状态。
  • 可以实现应用程序的以下功能:
  1. Undo(撤销)
  2. Redo(重做)
  3. History(历史记录)
  4. Snapshot(快照)

Memento模式中的登场角色

  • Originator(生成者)

       Originator角色会在保存自己的最新状态时生成Memento角色。当把以前保存的Memento角色传递给Originator角色时,它会将自己恢复至生成该Memento角色的状态。

  • Memento(纪念品)

       Memento角色会将Originator角色的内部信息整合在一起。在Memento角色中虽然保存了Originator角色的信息,但它不会向外部公开这些信息。

       两种接口:(宽和窄指的是暴露信息的程度)

       1.wide interface——宽接口(API)

       指所有用于获取恢复对象状态信息的方法的集合。由于宽接口(API)会暴露所有Memento角色的内部信息,因此能够使用宽接口(API)的只有Originator角色。

       2.narrow interface——窄接口(API)

       可以通过窄接口(API)获取的Memento角色的内部信息非常有限,因此可以有效地防止信息泄露。

  • Caretaker(负责人)

       当Caretaker角色想要保存当前的Originator角色的状态时,会通知Originator角色。Originator角色在接收到通知后会生成Mementor角色的实例并将其返回给Caretaker角色。由于以后可能会用Mementor实例来将Originator恢复至原来的状态,因此Caretaker角色会一直保存Memento实例。

       Caretaker只能使用Memento的窄接口,无法访问Memento角色内部的所有信息。它只是将Originator角色生成的Mementor角色当作一个黑盒子保存起来。


Mementor模式的类图


拓展思路的要点

  • 两种接口(API)和可见性

       可以使用可见性来控制访问权限。

  • 需要多少个Memento

       根据需要可以保存多个Memento的实例,实现保存各个时间点的对象的状态。

  • Memento的有效期限是多久

       根据需求来保存,如果保存到文件中,可以永久保存。但是要注意版本问题。

  • 划分Caretaker角色和Originator角色的意义

       Caretaker角色的职责是决定何时拍摄快照,何时撤销及保存Memento角色。

       另一方面,Originator角色的职责则是生成Memento角色和使用接收到的Memento角色来恢复自己的状态。

       Caretaker角色与Originator角色的拥有不同的职责分担,当我们需要对应一下需求变更时,就可以完全不用修改Originator角色。

  1. 变更为可以多次撤销。
  2. 变更为不仅可以撤销,还可以将现在的状态保存在文件中。

相关的设计模式

  • Command模式(第22章)

       在使用Command模式处理命令时,可以使用Memento模式实现撤销功能。

       在Memento模式中,为了能够实现快照和撤销功能,保存了对象当前的状态。保存的信息只是在恢复状态时所需要的那部分信息。

       而在Protype模式中,会生成一个与当前实例完全相同的另外一个实例。这两个实例的内容完全一样。

  • State模式(第19章)

       在Memento模式中,是用“实例”表示状态。

       而在State模式中,则是用“类”表示状态。


代码

  • Originator(生成者)
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

// 表示游戏主人公的类
public class Gamer {

	private int money; // 所持金钱
	private List<String> fruits = new ArrayList<String>(); // 获得的水果
	private Random random = new Random();// 随机数生成器
	private static String[] fruitsname = { "苹果", "葡萄", "香蕉", "橘子" }; // 表示水果种类的数组

	public Gamer(int money) { // 构造函数
		this.money = money;
	}

	public int getMoney() { // 获取当前所持金钱
		return money;
	}

	public void bet() { // 投掷骰子进行游戏
		int dice = random.nextInt(6) + 1; // 掷骰子(返回0~5,再加1,正好1~6)
		if (dice == 1) { // 骰子结果为1时,增加所持金钱
			money += 100;
			System.out.println("所持金钱增加了。");
		} else if (dice == 2) { // 骰子结果为2时,所持金钱减半
			money /= 2;
			System.out.println("所持金钱减半了。");
		} else if (dice == 6) { // 骰子结果为6时,获得水果
			String f = getFruit();
			System.out.println("获得了水果(" + f + ")。");
			fruits.add(f);
		} else { // 骰子结果为3、4、5则什么都不会发生
			System.out.println("什么都没有发生。");
		}
	}

	public Memento createMemento() { // 拍摄快照
		Memento result = new Memento(money);
		Iterator<String> it = fruits.iterator();
		while (it.hasNext()) {
			String f = it.next();
			if (f.startsWith("好吃的")) { // 只保存好吃的水果
				result.addFruit(f);
			}
		}
		return result;
	}

	public void restoreMemento(Memento memento) { // 撤销
		this.money = memento.money;
		this.fruits = memento.getFruits();
	}

	@Override
	public String toString() { // 用字符串表示主人公状态
		return "Gamer [money=" + money + ", fruits=" + fruits + "]";
	}

	private String getFruit() { // 获得一个水果
		String prefix = "";
		if (random.nextBoolean()) { // 随机增加"好吃的"前缀
			prefix = "好吃的";
		}
		return prefix + fruitsname[random.nextInt(fruitsname.length)];
	}
}
  • Memento(纪念品)
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

// 表示Gamer(主人公)状态的类
public class Memento implements Serializable {

	private static final long serialVersionUID = 1L;

	int money; // 所持金钱
	ArrayList<String> fruits; // 获得的水果

	// private int number; //直接给实例变量赋值

	public int getMoney() { // 获取当前所持金钱(narrow interface)
		return money;
	}

	Memento(int money) { // 构造函数(wide interface)
		this.money = money;
		this.fruits = new ArrayList<String>();
	}

	void addFruit(String fruit) { // 添加水果(wide interface)
		fruits.add(fruit);
	}

	@SuppressWarnings("unchecked")
	List<String> getFruits() { // 获取当前所持所有水果(wide interface)
		return (List<String>) fruits.clone();
	}

	// int getNumber() {
	// return number;
	// }

}
  • Caretaker(负责人)
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;


public class Main {

	public static final String SAVE_FILE_NAME = "game.dat";

	public static void main(String[] args) {
		Gamer gamer = new Gamer(100); // 最初的所持金钱数为100
		Memento memento = loadMemento();
		if (memento != null) {
			System.out.println("读取上次保存文档开始游戏。");
			gamer.restoreMemento(memento);
		} else {
			System.out.println("新游戏。");
			memento = gamer.createMemento(); // 保存最初的状态
		}
		for (int i = 0; i < 100; i++) {
			System.out.println("==== " + i); // 显示掷骰子的字数
			System.out.println("当前状态:" + gamer); // 显示主人公现在的状态

			gamer.bet(); // 进行游戏

			System.out.println("所持金钱为" + gamer.getMoney() + "元。");
			// 决定如何处理Memento
			if (gamer.getMoney() > memento.getMoney()) {
				System.out.println("    (所持金钱增加了许多,因此保存游戏当前的状态)");
				memento = gamer.createMemento();
				saveMemento(memento);
			} else if (gamer.getMoney() < memento.getMoney() / 2) {
				System.out.println("    (所持金钱减少了许多,因此将游戏恢复至以前的状态)");
				gamer.restoreMemento(memento);
			}

			// 等待一段时间
			try {
				Thread.sleep(1000);
			} catch (Exception e) {
				e.printStackTrace();
			}
			System.out.println("");
		}
	}

	public static void saveMemento(Memento memento) {
		try (ObjectOutput out = new ObjectOutputStream(
				new DeflaterOutputStream(new FileOutputStream(SAVE_FILE_NAME)))) {
			out.writeObject(memento);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static Memento loadMemento() {
		Memento result = null;
		try (ObjectInput in = new ObjectInputStream(new InflaterInputStream(new FileInputStream(SAVE_FILE_NAME)))) {
			result = (Memento) in.readObject();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}
}

注:博客中的图片来自网上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值