备忘录模式
(文章目录)
备忘录模式是什么?在不违反包装原则的情况下,捕获物体的内部状态,并将其保存在物体外,以便以后恢复物体为以前的状态。 在我看来,这种模式的定义主要表达了两部分。部分是存储副本以便以后恢复。这部分很容易理解。另一部分是在不违反包装原则的情况下备份和恢复对象。
为什么要使用备忘录模式接下来,我将结合一个例子来解释这两个问题:
- 为什么存储和恢复副本会违反包装原则?
- 如何使备忘录模式不违反包装原则?
假设有这样的面试问题,我希望你能写一个小程序,接收命令线的输入。当用户输入文本时,程序将其添加到内存文本中;用户输入“: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 撤销类对象存储的文本。 再举一个例子。假设每当有数据变化时,我们都需要生成一个备份,以便以后恢复。如果需要备份的数据很大,无论是存储(内存或硬盘)的消耗还是时间的消耗,如此高频的备份都可能是不可接受的。为了解决这个问题,我们通常采用“低频全备份”和“高频增量备份”相结合的方法。 不用说,全备份类似于我们上面的例子,就是保存所有数据的“快照”。所谓“增量备份”,是指记录每次操作或数据变化。 当我们需要在某个时间点恢复备份时,如果这个时间点有全部备份,我们可以直接恢复。如果此时点没有相应的全备份,我们将首先找到最新的全备份,然后使用它进行恢复,然后执行此时点之间的全备份和所有增量备份,即相应的操作或数据变化。这样可以减少全备份的数量和频率,减少时间和内存的消耗。
