在使用文本编辑器编写文件时,如果不小心删除了某句话,可以通过撤销(undo)功能将文件恢复至之前的状态,通过多次撤销,能够恢复至很久之前的版本。
使用面向对象编程的方式实现撤销功能时,需要实现保存实例的相关状态信息。然后,在撤销时,还需要根据所保存的信息将实例恢复至原来的状态。
要想恢复实例,需要一个可以自由访问实例内部结构的权限。但是,稍不注意,又可能会将依赖于实例内部结构的代码分散地编写在程序的各个地方,导致程序变得难以维护。破坏了程序的“封装性”。
通过引入表示实例状态的角色,可以在保存和恢复实例时有效地防止对象的封装性遭到破坏。
使用Memento模式可以实现应用程序的以下功能:
- Undo(撤销)
- Redo(重做)
- History(历史记录)
- Snapshot(快照)
可以事先将某个时间点的实例的状态保存下来,之后在有必要的时候,再将实例恢复至当时的状态。
示例程序:
一个收集水果和获取金钱数的掷骰子游戏。
- 游戏是自动进行的;
- 游戏的主人公通过掷骰子来决定下一个状态;
- 当骰子点数为1的时候,主人公的金钱会增加;
- 当骰子点数为2的时候,主人公的金钱会减少;
- 当骰子点数为6的时候,主人公会得到水果;
- 主人公没有钱的时候游戏会结束。
在程序中,如果金钱增加,为了方便将来恢复状态,我们会生成Memento类的实例,将现在的状态保存起来。所保存的数据为当前持有的金钱和水果。如果不断减少金钱,为了防止金钱变为0而结束游戏,会使用Memento的实例将游戏恢复至之前的状态。
| 名字 | 说明 |
|---|---|
| Memento | 表明Gamer状态的类 |
| Gamer | 表示游戏主人公的类。它会生成Memento的实例 |
| Main | 进行游戏的类,会实现保存Memento的实例,之后hi根据需要恢复Gamer的状态 |
Memento类:
表示Gamer状态的类。需要注意的是,Memento类的构造函数和addFruit方法的可见性都不是public,而是 default,是为了不让从game包外部改变Memento内部的状态。
public class Memento {int money;ArrayList fruits;public int getMoney() { // 获取当前所持金钱return money;}public Memento(int money) { // 构造函数this.money = money;this.fruits = new ArrayList();}void addFruit(String fruit) { // 添加水果fruits.add(fruit);}public ArrayList getFruits() { // 获取当前所持所有水果return (ArrayList) fruits.clone();}}
Gamer类:
表示游戏主人公的类,拥有金钱和水果属性,bet方法是开始游戏,createMemento是保存当前的状态,restoreMemento是恢复之前保存的状态。
public class Gamer {private int money;private List fruits = new ArrayList();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;if (dice == 1) {money += 100;System.out.println("所持金钱增加了。");} else if (dice == 2) {money /= 2;System.out.println("所持金钱减半了。");} else if (dice == 6) {String fruit = getFruit();System.out.println("获取了水果("+fruit+")。");fruits.add(fruit);}else {System.out.println("什么都没有发生");}}public Memento createMemento() { // 拍摄快照Memento memento = new Memento(money);Iterator iterator = fruits.iterator();while (iterator.hasNext()){String next = (String)iterator.next();if (next.startsWith("好吃的")) {memento.addFruit(next);}}return memento;}public void restoreMemento(Memento memento) { // 撤销this.money = memento.getMoney();this.fruits = memento.getFruits();}private String getFruit() {String prefix = "";if (random.nextBoolean()) {prefix = "好吃的";}return prefix + fruitsName[random.nextInt(fruitsName.length)];}@Overridepublic String toString() {return "Gamer{" +"money=" + money +", fruits=" + fruits +'}';}}
Main类:
public class Main {public static void main(String[] args) {Gamer gamer = new Gamer(100);Memento 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() + "元。");// 决定如何处理Mementoif (gamer.getMoney() > memento.getMoney()) {System.out.println("所持的金钱增加了,因此保存游戏当前的状态");memento = gamer.createMemento();} else if (gamer.getMoney() < memento.getMoney()) {System.out.println("所持的金钱减少了,因此将游戏恢复至以前的状态");gamer.restoreMemento(memento);}try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("");}}}
Memento模式中的登场角色:
Originator(生成者):
Originator角色会在保存自己的最新状态时生成Memento角色。当把以前保存的Memento角色传递给Originator角色时,它会将自己恢复至生成该Memento角色时的状态。
Memento(纪念品):
Memento角色会将Originator角色的内部信息整合在一起。在Memento角色中虽然保存了Originator角色的信息,但它不会向外部公开这些信息。
Memento角色有以下两种接口(API):
- wide interface——宽接口
“宽接口”是指所有用于获取恢复对象状态信息的方法的集合。由于宽接口(API)会暴露所有Memento角色的内部信息,因此能够使用宽接口的只有Originator。
- narrowinterface——窄接口
Memento角色为外部的caretaker角色提供了“窄接口”。可以通过窄接口获取的Memento角色的内部信息非常有限,因此可以有限地防止信息泄漏。
通过对外提供以上两种接口(API),可以有效地防止对象的封装性被破坏。
Caretaker(负责人):
当Caretaker角色想要保存当前的Originator角色的实例时,会通知Originator角色。Originator角色在接收到通知后会生成Memento角色的实例并将其返回给Caretaker角色。由于以后可能会用Memento来将Originator恢复至原来的状态,因此Caretaker角色会一直保存Memento实例。
不过,Caretaker角色中只能使用Memento角色两个接口(API)中的窄接口(API),也就是说它无法访问Memento角色内部的所有信息。它只是将Originator角色生成的Memento角色当做一个黑盒子保存起来。
虽然Originator和Memento角色之间是强关联关系,但Caretaker角色和Memento之间是弱关联关系。Memento对Caretaker隐藏了自身的内部关系。
拓展思路的要点:
需要多少个Memento:
示例程序中,Main中只保存了一个Memento,如果在Main类中使用数组等集合,让他可以保存多个Memento类的实例,就可以实现保存各个时间点的对象的状态。
Memento的有效期限是多久:
是在内存中保存Memento的,这样并没有什么问题。但是,如果要将Memento永久保存在文件中,就会出现有效期限的问题了。
这是由于可能出现应用程序升级导致版本不匹配的问题。
相关的设计模式:
Command模式:
在使用Command模式处理命令时,可以使用Memento模式实现撤销功能。
Protype模式:
在Memento模式中,为了能够实现快照和撤销功能,保存了对象当前的状态,保存的消息只是在恢复状态时所需要的那部分信息。
而在Protype模式中,会生成一个与当前实例完全相同的另外一个实例,这两个实例的内容完全一样。
State模式:
在Memento模式中,是用“实例”表示状态;
而在State模式中,则是用“类”表示状态;
问题:
caretaker角色只能通过窄接口来操作Memento角色。如果Caretaker角色可以随意地操作Memento角色,会发生什么问题?
这会导致Caretaker角色失去与Originator角色和Memento角色之间的独立性。
如果Caretaker角色可以随意地操作Memento角色,当要修改Originator角色的内部的代码时,就必须要同样地修改Caretaker角色。
