备忘录模式
每个人都有犯错误的时候,都希望有种“后悔药”能弥补自己的过失,让自己重新开始,但现实是残酷的。在计算机应用中,客户同样会常常犯错误,能否提供“后悔药”给他们呢?当然是可以的,而且是有必要的。这个功能由“备忘录模式”来实现。
其实很多应用软件都提供了这项功能,如 Word、记事本、Photoshop、Eclipse 等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 IE 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。
备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。
模式的定义与特点
备忘录(Memento)模式的定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。
备忘录模式是一种对象行为型模式,其主要优点如下。
- 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
- 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
其主要缺点是:资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
模式的结构与实现
备忘录模式的核心是设计备忘录类以及用于管理备忘录的管理者类,现在我们来学习其结构与实现。
1. 模式的结构
备忘录模式的主要角色如下。
- 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
- 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
- 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
备忘录模式的结构图如图 1 所示。

图1 备忘录模式的结构图
2. 模式的实现
备忘录模式的实现代码如下:
1. package memento;
2. public class MementoPattern
3. {
4. public static void main(String[] args)
5. {
6. Originator or=new Originator();
7. Caretaker cr=new Caretaker();
8. or.setState("S0");
9. System.out.println("初始状态:"+or.getState());
10. cr.setMemento(or.createMemento()); //保存状态
11. or.setState("S1");
12. System.out.println("新的状态:"+or.getState());
13. or.restoreMemento(cr.getMemento()); //恢复状态
14. System.out.println("恢复状态:"+or.getState());
15. }
16. }
17. //备忘录
18. class Memento
19. {
20. private String state;
21. public Memento(String state)
22. {
23. this.state=state;
24. }
25. public void setState(String state)
26. {
27. this.state=state;
28. }
29. public String getState()
30. {
31. return state;
32. }
33. }
34. //发起人
35. class Originator
36. {
37. private String state;
38. public void setState(String state)
39. {
40. this.state=state;
41. }
42. public String getState()
43. {
44. return state;
45. }
46. public Memento createMemento()
47. {
48. return new Memento(state);
49. }
50. public void restoreMemento(Memento m)
51. {
52. this.setState(m.getState());
53. }
54. }
55. //管理者
56. class Caretaker
57. {
58. private Memento memento;
59. public void setMemento(Memento m)
60. {
61. memento=m;
62. }
63. public Memento getMemento()
64. {
65. return memento;
66. }
67. }
程序运行的结果如下:
<pre class="info-box">初始状态:S0
新的状态:S1
恢复状态:S0</pre>
模式的应用实例
【例1】利用备忘录模式设计相亲游戏。
分析:假如有西施、王昭君、貂蝉、杨玉环四大美女同你相亲,你可以选择其中一位作为你的爱人;当然,如果你对前面的选择不满意,还可以重新选择,但希望你不要太花心;这个游戏提供后悔功能,用“备忘录模式”设计比较合适(点此下载所要显示的四大美女的图片)。
首先,先设计一个美女(Girl)类,它是备忘录角色,提供了获取和存储美女信息的功能;然后,设计一个相亲者(You)类,它是发起人角色,它记录当 前时刻的内部状态信息(临时妻子的姓名),并提供创建备忘录和恢复备忘录数据的功能;最后,定义一个美女栈(GirlStack)类,它是管理者角色,负责对备忘录进行管理,用于保存相亲者(You)前面选过的美女信息,不过最多只能保存 4 个,提供后悔功能。
客户类设计成窗体程序,它包含美女栈(GirlStack)对象和相亲者(You)对象,它实现了 ActionListener 接口的事件处理方法 actionPerformed(ActionEvent e),并将 4 大美女图像和相亲者(You)选择的美女图像在窗体中显示出来。图 2 所示是其结构图。

图2 相亲游戏的结构图
程序代码如下:
1. package memento;
2. import java.awt.GridLayout;
3. import java.awt.event.*;
4. import javax.swing.*;
5. public class DatingGame
6. {
7. public static void main(String[] args)
8. {
9. new DatingGameWin();
10. }
11. }
12. //客户窗体类
13. class DatingGameWin extends JFrame implements ActionListener
14. {
15. private static final long serialVersionUID=1L;
16. JPanel CenterJP,EastJP;
17. JRadioButton girl1,girl2,girl3,girl4;
18. JButton button1,button2;
19. String FileName;
20. JLabel g;
21. You you;
22. GirlStack girls;
23. DatingGameWin()
24. {
25. super("利用备忘录模式设计相亲游戏");
26. you=new You();
27. girls=new GirlStack();
28. this.setBounds(0,0,900,380);
29. this.setResizable(false);
30. FileName="src/memento/Photo/四大美女.jpg";
31. g=new JLabel(new ImageIcon(FileName),JLabel.CENTER);
32. CenterJP=new JPanel();
33. CenterJP.setLayout(new GridLayout(1,4));
34. CenterJP.setBorder(BorderFactory.createTitledBorder("四大美女如下:"));
35. CenterJP.add(g);
36. this.add("Center",CenterJP);
37. EastJP=new JPanel();
38. EastJP.setLayout(new GridLayout(1,1));
39. EastJP.setBorder(BorderFactory.createTitledBorder("您选择的爱人是:"));
40. this.add("East",EastJP);
41. JPanel SouthJP=new JPanel();
42. JLabel info=new JLabel("四大美女有“沉鱼落雁之容、闭月羞花之貌”,您选择谁?");
43. girl1=new JRadioButton("西施",true);
44. girl2=new JRadioButton("貂蝉");
45. girl3=new JRadioButton("王昭君");
46. girl4=new JRadioButton("杨玉环");
47. button1=new JButton("确定");
48. button2=new JButton("返回");
49. ButtonGroup group=new ButtonGroup();
50. group.add(girl1);
51. group.add(girl2);
52. group.add(girl3);
53. group.add(girl4);
54. SouthJP.add(info);
55. SouthJP.add(girl1);
56. SouthJP.add(girl2);
57. SouthJP.add(girl3);
58. SouthJP.add(girl4);
59. SouthJP.add(button1);
60. SouthJP.add(button2);
61. button1.addActionListener(this);
62. button2.addActionListener(this);
63. this.add("South",SouthJP);
64. showPicture("空白");
65. you.setWife("空白");
66. girls.push(you.createMemento()); //保存状态
67. }
68. //显示图片
69. void showPicture(String name)
70. {
71. EastJP.removeAll(); //清除面板内容
72. EastJP.repaint(); //刷新屏幕
73. you.setWife(name);
74. FileName="src/memento/Photo/"+name+".jpg";
75. g=new JLabel(new ImageIcon(FileName),JLabel.CENTER);
76. EastJP.add(g);
77. this.setVisible(true);
78. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
79. }
80. @Override
81. public void actionPerformed(ActionEvent e)
82. {
83. boolean ok=false;
84. if(e.getSource()==button1)
85. {
86. ok=girls.push(you.createMemento()); //保存状态
87. if(ok && girl1.isSelected())
88. {
89. showPicture("西施");
90. }
91. else if(ok && girl2.isSelected())
92. {
93. showPicture("貂蝉");
94. }
95. else if(ok && girl3.isSelected())
96. {
97. showPicture("王昭君");
98. }
99. else if(ok && girl4.isSelected())
100. {
101. showPicture("杨玉环");
102. }
103. }
104. else if(e.getSource()==button2)
105. {
106. you.restoreMemento(girls.pop()); //恢复状态
107. showPicture(you.getWife());
108. }
109. }
110. }
111. //备忘录:美女
112. class Girl
113. {
114. private String name;
115. public Girl(String name)
116. {
117. this.name=name;
118. }
119. public void setName(String name)
120. {
121. this.name=name;
122. }
123. public String getName()
124. {
125. return name;
126. }
127. }
128. //发起人:您
129. class You
130. {
131. private String wifeName; //妻子
132. public void setWife(String name)
133. {
134. wifeName=name;
135. }
136. public String getWife()
137. {
138. return wifeName;
139. }
140. public Girl createMemento()
141. {
142. return new Girl(wifeName);
143. }
144. public void restoreMemento(Girl p)
145. {
146. setWife(p.getName());
147. }
148. }
149. //管理者:美女栈
150. class GirlStack
151. {
152. private Girl girl[];
153. private int top;
154. GirlStack()
155. {
156. girl=new Girl[5];
157. top=-1;
158. }
159. public boolean push(Girl p)
160. {
161. if(top>=4)
162. {
163. System.out.println("你太花心了,变来变去的!");
164. return false;
165. }
166. else
167. {
168. girl[++top]=p;
169. return true;
170. }
171. }
172. public Girl pop()
173. {
174. if(top<=0)
175. {
176. System.out.println("美女栈空了!");
177. return girl[0];
178. }
179. else return girl[top--];
180. }
181. }
程序运行结果如图 3 所示。

图3 相亲游戏的运行结果(点此查看原图)
模式的应用场景
前面学习了备忘录模式的定义与特点、结构与实现,现在来看该模式的以下应用场景。
- 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
- 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,Eclipse 等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。
模式的扩展
在前面介绍的备忘录模式中,有单状态备份的例子,也有多状态备份的例子。下面介绍备忘录模式如何同原型模式混合使用。在备忘录模式中,通过定义“备忘录”来备份“发起人”的信息,而原型模式的 clone() 方法具有自备份功能,所以,如果让发起人实现 Cloneable 接口就有备份自己的功能,这时可以删除备忘录类,其结构图如图 4 所示。

图4 带原型的备忘录模式的结构图
实现代码如下:
1. package memento;
2. public class PrototypeMemento
3. {
4. public static void main(String[] args)
5. {
6. OriginatorPrototype or=new OriginatorPrototype();
7. PrototypeCaretaker cr=new PrototypeCaretaker();
8. or.setState("S0");
9. System.out.println("初始状态:"+or.getState());
10. cr.setMemento(or.createMemento()); //保存状态
11. or.setState("S1");
12. System.out.println("新的状态:"+or.getState());
13. or.restoreMemento(cr.getMemento()); //恢复状态
14. System.out.println("恢复状态:"+or.getState());
15. }
16. }
17. //发起人原型
18. class OriginatorPrototype implements Cloneable
19. {
20. private String state;
21. public void setState(String state)
22. {
23. this.state=state;
24. }
25. public String getState()
26. {
27. return state;
28. }
29. public OriginatorPrototype createMemento()
30. {
31. return this.clone();
32. }
33. public void restoreMemento(OriginatorPrototype opt)
34. {
35. this.setState(opt.getState());
36. }
37. public OriginatorPrototype clone()
38. {
39. try
40. {
41. return (OriginatorPrototype) super.clone();
42. }
43. catch(CloneNotSupportedException e)
44. {
45. e.printStackTrace();
46. }
47. return null;
48. }
49. }
50. //原型管理者
51. class PrototypeCaretaker
52. {
53. private OriginatorPrototype opt;
54. public void setMemento(OriginatorPrototype opt)
55. {
56. this.opt=opt;
57. }
58. public OriginatorPrototype getMemento()
59. {
60. return opt;
61. }
}
程序的运行结果如下:
初始状态:S0
新的状态:S1
恢复状态:S0