【图解设计模式】Memento模式

引入表示实例状态的角色,在保存和恢复实例时有效地防止对象的封装性遭到破坏。

示例

一个收集水果和获取金钱数的掷骰子游戏,游戏规则很简单,具体如下。

  • 游戏是自动进行的
  • 游戏的主人公通过掷骰子来决定下一个状态
  • 当骰子点数为1的时候,主人公的金钱会增加
  • 当骰子点数为2的时候,主人公的金钱会减少
  • 当骰子点数为6的时候,主人公会得到水果
  • 主人公没有钱时游戏就会结束

类图

mWXbyn.png

时序图

mWjkex.png

Memento类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package game;

import java.util.ArrayList;
import java.util.List;

public class Memento {
int money;
ArrayList fruits;

Memento(int money) {
this.money = money;
this.fruits = new ArrayList();
}

public int getMoney() {
return money;
}

List getFruits() {
return (List) fruits.clone();
}

void addFruit(String fruit) {
fruits.add(fruit);
}
}

Gamer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package game;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

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 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 f = getFruit();
System.out.println("获得了水果(" + f + ")。");
fruits.add(f);
}
else
System.out.println("什么都没有发生。");
}

public Memento createMemento() {
Memento m = new Memento(money);

Iterator it = fruits.iterator();

while (it.hasNext()) {
String f = (String) it.next();

if (f.startsWith("好吃的"))
m.addFruit(f);
}

return m;
}

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

private String getFruit() {
String prefix = "";

if (random.nextBoolean())
prefix = "好吃的";

return prefix + fruitsname[random.nextInt(fruitsname.length)];
}

public int getMoney() {
return money;
}

@Override
public String toString() {
return "[money = " + money + ", fruits = " + fruits + "]";
}
}

Main类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import game.Gamer;
import game.Memento;

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() + "元。");

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

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}

System.out.println("");
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
==== 0
当前状态:[money = 100, fruits = []]
获得了水果(好吃的葡萄)。
所持金钱为100元。
==== 1
当前状态:[money = 100, fruits = [好吃的葡萄]]
所持金钱减半了。
所持金钱为50元。
==== 2
当前状态:[money = 50, fruits = [好吃的葡萄]]
什么都没有发生。
所持金钱为50元。
==== 3
当前状态:[money = 50, fruits = [好吃的葡萄]]
什么都没有发生。
所持金钱为50元。
==== 4
当前状态:[money = 50, fruits = [好吃的葡萄]]
获得了水果(香蕉)。
所持金钱为50元。
==== 5
当前状态:[money = 50, fruits = [好吃的葡萄, 香蕉]]
什么都没有发生。
所持金钱为50元。
==== 6
当前状态:[money = 50, fruits = [好吃的葡萄, 香蕉]]
什么都没有发生。
所持金钱为50元。
==== 7
当前状态:[money = 50, fruits = [好吃的葡萄, 香蕉]]
所持金钱减半了。
所持金钱为25元。
(所持金钱减少了许多,因此将游戏恢复至以前的状态)
==== 8
当前状态:[money = 100, fruits = []]
所持金钱增加了。
所持金钱为200元。
(所持金钱增加了许多,因此保存游戏当前的状态)
==== 9
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。

登场角色

Originator(生成者)

Originator角色会在保存自己的最新状态时生成Memento角色。当把以前保存的Memento角色传递给Originator角色时,它会将自己恢复至生成该Memento角色时的状态。在示例程序中,由Gamer类扮演此角色。

Memento(纪念品)

Memento角色会将Originator角色的内部信息整合在一起。在Memento角色中虽然保存了Originator角色的信息,但它不会向外部公开这些信息。
Memento角色有以下两种接口(API)。

  • wide interface——宽接口(API)

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

  • narrow interface——窄接口(API)

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

通过对外提供以上两种接口(API),可以有效地防止对象的封装性被破坏。
在示例程序中,由Memento类扮演此角色。
Originator角色和Memento角色之间有着非常紧密的联系。

Caretaker(负责人)

当Caretaker角色想要保存当前的Originator角色的状态时,会通知Originator角色。Originator角色在接收到通知后会生成Memento角色的实例并将其返回给Caretaker角色。由于以后可能会用Memento实例来将Originator恢复至原来的状态,因此Caretaker角色会一直保存Memento实例。在示例程序中,由Main类扮演此角色。
不过,Caretaker角色只能使用Memento角色两种接口(API)中的窄接口(API),也就是说它无法访问Memento角色内部的所有信息。它只是将Originator角色生成的Memento角色当作一个黑盒子保存起来。
虽然Originator角色和Memento角色之间是强关联关系,但Caretaker角色和Memento角色之间是弱关联关系。Memento角色对Caretaker角色隐藏了自身的内部信息。

类图

mfXRH0.png