1、什么是备忘录模式?

备忘录模式,也叫快照(Snapshot)模式,英文翻译是 Memento Design Pattern。在 GoF 的《设计模式》一书中,备忘录模式是这么定义的:

Captures and externalizes an object’s internal state so that it can be restored later, all without violating encapsulation.

翻译成中文就是:在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。

在我看来,这个模式的定义主要表达了两部分内容。一部分是,存储副本以便后期恢复。这一部分很好理解。另一部分是,要在不违背封装原则的前提下,进行对象的备份和恢复。这部分不太好理解。

2、为什么要使用备忘录模式

备忘录模式的应用场景也比较明确和有限,主要是用来防丢失、撤销、恢复等。它跟平时我们常说的“备份”很相似。两者的主要区别在于,备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计。

对于大对象的备份来说,备份占用的存储空间会比较大,备份和恢复的耗时会比较长。针对这个问题,不同的业务场景有不同的处理方式。比如,只备份必要的恢复信息,结合最新的数据来恢复;再比如,全量备份和增量备份相结合,低频全量备份,高频增量备份,两者结合来做恢复。

3、例子

3.1、GoF(简单)

image.png

  1. import java.util.ArrayList;
  2. import java.util.List;
  3. public class Main {
  4. public static void main(String[] args) {
  5. Originator originator = new Originator();
  6. Caretaker caretaker = new Caretaker();
  7. // 设置初始状态
  8. originator.setState("On");
  9. originator.show();
  10. // 备份
  11. caretaker.add(originator.createMemento());
  12. // 修改初始装填
  13. originator.setState("Off");
  14. originator.show();
  15. // 恢复备份
  16. originator.setMemento(caretaker.get(0));
  17. originator.show();
  18. }
  19. }
  20. /**
  21. * Memento 备忘录类
  22. */
  23. class Memento {
  24. private String state;
  25. public Memento(String state) {
  26. this.state = state;
  27. }
  28. public String getState() {
  29. return state;
  30. }
  31. }
  32. /**
  33. * 发起人(Originator)类
  34. */
  35. class Originator {
  36. /**
  37. * 需要保存的属性,可能有多个
  38. */
  39. private String state;
  40. public String getState() {
  41. return state;
  42. }
  43. public void setState(String state) {
  44. this.state = state;
  45. }
  46. /**
  47. * 创建备忘录
  48. */
  49. public Memento createMemento() {
  50. return new Memento(state);
  51. }
  52. /**
  53. * 恢复备忘录
  54. */
  55. public void setMemento(Memento memento) {
  56. state = memento.getState();
  57. }
  58. public void show() {
  59. System.out.println("state = " + state);
  60. }
  61. }
  62. /**
  63. * Caretaker 管理者类
  64. */
  65. class Caretaker {
  66. private List<Memento> list = new ArrayList<>();
  67. public void add(Memento memento) {
  68. list.add(memento);
  69. }
  70. public Memento get(int index) {
  71. return list.get(index);
  72. }
  73. }

image.png

4、总结

备忘录模式也叫快照模式,具体来说,就是在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。这个模式的定义表达了两部分内容:一部分是,存储副本以便后期恢复;另一部分是,要在不违背封装原则的前提下,进行对象的备份和恢复。

备忘录模式的应用场景也比较明确和有限,主要是用来防丢失、撤销、恢复等。它跟平时我们常说的“备份”很相似。两者的主要区别在于,备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计

对于大对象的备份来说,备份占用的存储空间会比较大,备份和恢复的耗时会比较长。针对这个问题,不同的业务场景有不同的处理方式。比如,只备份必要的恢复信息,结合最新的数据来恢复;再比如,全量备份和增量备份相结合,低频全量备份,高频增量备份,两者结合来做恢复。

5、问题

5.1、为什么存储和恢复副本会违背封装原则?

5.2、备忘录模式是如何做到不违背封装原则的?

5.3、如何优化内存和时间消耗?

前面我们只是简单介绍了备忘录模式的原理和经典实现,现在我们再继续深挖一下。如果要备份的对象数据比较大,备份频率又比较高,那快照占用的内存会比较大,备份和恢复的耗时会比较长。这个问题该如何解决呢?

不同的应用场景下有不同的解决方法。比如,我们前面举的那个例子,应用场景是利用备忘录来实现撤销操作,而且仅仅支持顺序撤销,也就是说,每次操作只能撤销上一次的输入,不能跳过上次输入撤销之前的输入。在具有这样特点的应用场景下,为了节省内存,我们不需要在快照中存储完整的文本,只需要记录少许信息,比如在获取快照当下的文本长度,用这个值结合 InputText 类对象存储的文本来做撤销操作。

我们再举一个例子。假设每当有数据改动,我们都需要生成一个备份,以备之后恢复。如果需要备份的数据很大,这样高频率的备份,不管是对存储(内存或者硬盘)的消耗,还是对时间的消耗,都可能是无法接受的。想要解决这个问题,我们一般会采用“低频率全量备份”和“高频率增量备份”相结合的方法。

全量备份就不用讲了,它跟我们上面的例子类似,就是把所有的数据“拍个快照”保存下来。所谓“增量备份”,指的是记录每次操作或数据变动。

当我们需要恢复到某一时间点的备份的时候,如果这一时间点有做全量备份,我们直接拿来恢复就可以了。如果这一时间点没有对应的全量备份,我们就先找到最近的一次全量备份,然后用它来恢复,之后执行此次全量备份跟这一时间点之间的所有增量备份,也就是对应的操作或者数据变动。这样就能减少全量备份的数量和频率,减少对时间、内存的消耗。