1、原由

每个人都有犯错误的时候,都希望有种“后悔药”能弥补自己的过失,让自己重新开始,但现实是残酷的。在计算机应用中,客户同样会常常犯错误,能否提供“后悔药”给他们呢?当然是可以的,而且是有必要的。这个功能由“备忘录模式”来实现。

其实很多应用软件都提供了这项功能,如 Word、记事本、Photoshop、Eclipse 等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 IE 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。

2、定义

备忘录(Memento)模式的定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。

3、优点

  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。

4、缺点

资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

5、结构

备忘录模式的核心是设计备忘录类以及用于管理备忘录的管理者类,现在我们来学习其结构与实现。
备忘录模式的主要角色如下。

  1. 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  2. 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  3. 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。


备忘录模式的结构图如图 1 所示。
image.png

6、实现

备忘录模式的实现代码如下:

  1. package net.biancheng.c.memento;
  2. public class MementoPattern {
  3. public static void main(String[] args) {
  4. Originator or = new Originator();
  5. Caretaker cr = new Caretaker();
  6. or.setState("S0");
  7. System.out.println("初始状态:" + or.getState());
  8. cr.setMemento(or.createMemento()); //保存状态
  9. or.setState("S1");
  10. System.out.println("新的状态:" + or.getState());
  11. or.restoreMemento(cr.getMemento()); //恢复状态
  12. System.out.println("恢复状态:" + or.getState());
  13. }
  14. }
  15. //备忘录
  16. class Memento {
  17. private String state;
  18. public Memento(String state) {
  19. this.state = state;
  20. }
  21. public void setState(String state) {
  22. this.state = state;
  23. }
  24. public String getState() {
  25. return state;
  26. }
  27. }
  28. //发起人
  29. class Originator {
  30. private String state;
  31. public void setState(String state) {
  32. this.state = state;
  33. }
  34. public String getState() {
  35. return state;
  36. }
  37. public Memento createMemento() {
  38. return new Memento(state);
  39. }
  40. public void restoreMemento(Memento m) {
  41. this.setState(m.getState());
  42. }
  43. }
  44. //管理者
  45. class Caretaker {
  46. private Memento memento;
  47. public void setMemento(Memento m) {
  48. memento = m;
  49. }
  50. public Memento getMemento() {
  51. return memento;
  52. }
  53. }

程序运行的结果如下:
image.png

7、应用实例

1、利用备忘录模式设计相亲游戏

分析:假如有西施、王昭君、貂蝉、杨玉环四大美女同你相亲,你可以选择其中一位作为你的爱人;当然,如果你对前面的选择不满意,还可以重新选择,但希望你不要太花心;这个游戏提供后悔功能,用“备忘录模式”设计比较合适(点此下载所要显示的四大美女的图片)。

首先,先设计一个美女(Girl)类,它是备忘录角色,提供了获取和存储美女信息的功能;然后,设计一个相亲者(You)类,它是发起人角色,它记录当前时刻的内部状态信息(临时妻子的姓名),并提供创建备忘录和恢复备忘录数据的功能;最后,定义一个美女栈(GirlStack)类,它是管理者角色,负责对备忘录进行管理,用于保存相亲者(You)前面选过的美女信息,不过最多只能保存 4 个,提供后悔功能。

客户类设计成窗体程序,它包含美女栈(GirlStack)对象和相亲者(You)对象,它实现了 ActionListener 接口的事件处理方法 actionPerformed(ActionEvent e),并将 4 大美女图像和相亲者(You)选择的美女图像在窗体中显示出来。图 2 所示是其结构图。
image.png
程序代码如下:

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

程序运行结果如图 3 所示
image.png

8、应用场景

前面学习了备忘录模式的定义与特点、结构与实现,现在来看该模式的以下应用场景。

  1. 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
  2. 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,Eclipse 等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。

9、扩展

在前面介绍的备忘录模式中,有单状态备份的例子,也有多状态备份的例子。下面介绍备忘录模式如何同原型模式混合使用。在备忘录模式中,通过定义“备忘录”来备份“发起人”的信息,而原型模式的 clone() 方法具有自备份功能,所以,如果让发起人实现 Cloneable 接口就有备份自己的功能,这时可以删除备忘录类,其结构图如图 4 所示。
image.png
实现代码如下:

  1. package net.biancheng.c.memento;
  2. public class PrototypeMemento {
  3. public static void main(String[] args) {
  4. OriginatorPrototype or = new OriginatorPrototype();
  5. PrototypeCaretaker cr = new PrototypeCaretaker();
  6. or.setState("S0");
  7. System.out.println("初始状态:" + or.getState());
  8. cr.setMemento(or.createMemento()); //保存状态
  9. or.setState("S1");
  10. System.out.println("新的状态:" + or.getState());
  11. or.restoreMemento(cr.getMemento()); //恢复状态
  12. System.out.println("恢复状态:" + or.getState());
  13. }
  14. }
  15. //发起人原型
  16. class OriginatorPrototype implements Cloneable {
  17. private String state;
  18. public void setState(String state) {
  19. this.state = state;
  20. }
  21. public String getState() {
  22. return state;
  23. }
  24. public OriginatorPrototype createMemento() {
  25. return this.clone();
  26. }
  27. public void restoreMemento(OriginatorPrototype opt) {
  28. this.setState(opt.getState());
  29. }
  30. public OriginatorPrototype clone() {
  31. try {
  32. return (OriginatorPrototype) super.clone();
  33. } catch (CloneNotSupportedException e) {
  34. e.printStackTrace();
  35. }
  36. return null;
  37. }
  38. }
  39. //原型管理者
  40. class PrototypeCaretaker {
  41. private OriginatorPrototype opt;
  42. public void setMemento(OriginatorPrototype opt) {
  43. this.opt = opt;
  44. }
  45. public OriginatorPrototype getMemento() {
  46. return opt;
  47. }
  48. }

程序的运行结果如下:
image.png

拓展

由于 JDK、SpringMybatis 中很少有备忘录模式,所以该设计模式不做典型应用源码分析。

Spring Webflow 中 DefaultMessageContext 类实现了 StateManageableMessageContext 接口,查看其源码可以发现其主要逻辑就相当于给 Message 备份。感兴趣的小伙伴可以去阅读学习其源码。

10、进阶阅读

如果您想了解备忘录模式在实际项目中的应用,可猛击阅读《使用备忘录模式实现草稿箱功能》文章。