当前位置: 首页 > 图灵资讯 > 技术篇> 设计模式- 备忘录模式

设计模式- 备忘录模式

来源:图灵教育
时间:2023-09-05 11:56:23

备忘录模式

(文章目录)

备忘录模式是什么?

  在不违反包装原则的情况下,捕获物体的内部状态,并将其保存在物体外,以便以后恢复物体为以前的状态。  在我看来,这种模式的定义主要表达了两部分。部分是存储副本以便以后恢复。这部分很容易理解。另一部分是在不违反包装原则的情况下备份和恢复对象。

为什么要使用备忘录模式

  接下来,我将结合一个例子来解释这两个问题:

  • 为什么存储和恢复副本会违反包装原则?
  • 如何使备忘录模式不违反包装原则?

  假设有这样的面试问题,我希望你能写一个小程序,接收命令线的输入。当用户输入文本时,程序将其添加到内存文本中;用户输入“:list程序在命令行中输出内存文本的内容;用户输入“:undo程序将取消上次输入的文本,即从内存文本中删除上次输入的文本。 我举了一个小例子来解释这个需求,如下所示:

>hello>:listhello>world>:listhelloworld>:undo>:listhello

  如何实现编程?您可以打开它 IDE 先试着自己写,然后看下面的解释。总的来说,这个小程序并不复杂。我写了一个实现的想法,如下:

public class InputText {  private StringBuilder text = new StringBuilder();  public String getText() {    return text.toString();  }  public void append(String input) {    text.append(input);  }  public void setText(String text) {    this.text.replace(0, this.text.length(), text);  }}public class SnapshotHolder {  private Stack<InputText> snapshots = new Stack<>();  public InputText popSnapshot() {    return snapshots.pop();  }  public void pushSnapshot(InputText inputText) {    InputText deepClonedInputText = new InputText();    deepClonedInputText.setText(inputText.getText());    snapshots.push(deepClonedInputText);  }}public class ApplicationMain {  public static void main(String[] args) {    InputText inputText = new InputText();    SnapshotHolder snapshotsHolder = new SnapshotHolder();    Scanner scanner = new Scanner(System.in);    while (scanner.hasNext()) {      String input = scanner.next();      if (input.equals(":list")) {        System.out.println(inputText.getText());      } else if (input.equals(":undo")) {        InputText snapshot = snapshotsHolder.popSnapshot();        inputText.setText(snapshot.getText());      } else {        snapshotsHolder.pushSnapshot(inputText);        inputText.append(input);      }    }  }}

  事实上,备忘录模式的实现非常灵活,没有固定的实现方式。在不同的业务需求和编程语言下,代码实现可能不同。以上代码基本实现了备忘录最基本的功能。然而,如果我们深入研究,还有一些问题需要解决,即上述定义中提到的第二点:备份和恢复对象,而不违反包装原则。上述代码对此并不满意,主要体现在以下两个方面:

  • 第一,为了用快照恢复 InputText 对象,我们在 InputText 类中定义了 setText() 函数,但该函数可能被其他业务使用,因此,不应暴露的函数违反了包装原则;
  • 第二,快照本身是不可改变的。理论上,它不应该包含任何内容 set() 修改内部状态的函数,但在上述代码实现中,“快照”业务模型被重复使用 InputText 类的定义,而 InputText 类本身有一系列修改内部状态的函数,因此,使用 InputText 类表示快照违反了包装原则。
如何使用备忘录模式

  针对上述问题,我们对代码进行了两点修改。一是定义一个独立的类别(Snapshot 类)表示快照,而不是重复使用 InputText 类。这类只暴露 get() 方法,没有 set() 任何修改内部状态的方法。其二,在 InputText 类中,我们把 setText() 该方法重命名为 restoreSnapshot() 方法,意图更清楚,只用于恢复对象。  按照这个想法,我们重构了代码。重构后的代码如下:

public class InputText {  private StringBuilder text = new StringBuilder();  public String getText() {    return text.toString();  }  public void append(String input) {    text.append(input);  }  public Snapshot createSnapshot() {    return new Snapshot(text.toString());  }  public void restoreSnapshot(Snapshot snapshot) {    this.text.replace(0, this.text.length(), snapshot.getText());  }}public class Snapshot {  private String text;  public Snapshot(String text) {    this.text = text;  }  public String getText() {    return this.text;  }}public class SnapshotHolder {  private Stack<Snapshot> snapshots = new Stack<>();  public Snapshot popSnapshot() {    return snapshots.pop();  }  public void pushSnapshot(Snapshot snapshot) {    snapshots.push(snapshot);  }}public class ApplicationMain {  public static void main(String[] args) {    InputText inputText = new InputText();    SnapshotHolder snapshotsHolder = new SnapshotHolder();    Scanner scanner = new Scanner(System.in);    while (scanner.hasNext()) {      String input = scanner.next();      if (input.equals(":list")) {        System.out.println(inputText.toString());      } else if (input.equals(":undo")) {        Snapshot snapshot = snapshotsHolder.popSnapshot();        inputText.restoreSnapshot(snapshot);      } else {        snapshotsHolder.pushSnapshot(inputText.createSnapshot());        inputText.append(input);      }    }  }}
总结

  我们只是简要介绍了备忘录模式的原理和经典实现,现在我们将继续深入挖掘。若要备份的对象数据较大,备份频率较高,则快照占用的内存较大,备份和恢复时间较长。如何解决这个问题?  不同的应用场景有不同的解决方案。例如,在我们之前提到的例子中,应用程序场景使用备忘录来取消操作,并且只支持顺序取消,也就是说,每个操作只能取消最后一个输入,而不能跳过最后一个输入取消前的输入。在具有这一特征的应用场景中,为了节省内存,我们不需要将完整的文本存储在快照中,只需要记录一点信息,如获取快照的当前文本长度,并将此值结合起来 InputText 撤销类对象存储的文本。  再举一个例子。假设每当有数据变化时,我们都需要生成一个备份,以便以后恢复。如果需要备份的数据很大,无论是存储(内存或硬盘)的消耗还是时间的消耗,如此高频的备份都可能是不可接受的。为了解决这个问题,我们通常采用“低频全备份”和“高频增量备份”相结合的方法。  不用说,全备份类似于我们上面的例子,就是保存所有数据的“快照”。所谓“增量备份”,是指记录每次操作或数据变化。  当我们需要在某个时间点恢复备份时,如果这个时间点有全部备份,我们可以直接恢复。如果此时点没有相应的全备份,我们将首先找到最新的全备份,然后使用它进行恢复,然后执行此时点之间的全备份和所有增量备份,即相应的操作或数据变化。这样可以减少全备份的数量和频率,减少时间和内存的消耗。