快照模式也叫做备忘录模式,但是我觉得如果是了解快照的话,我觉得比备忘录更形象一点,毕竟现在快照有各种,有页面快照,有系统快照等,相当于是一个备份。备忘录其实也是备份的意思,我觉得快照更贴切一点现在的描述。
快照模式是一种行为模式,行为模式可以说是设计模式最大头的一部分,作为行为的解耦,相比于创建型,结构型,实际上不改架构的单纯做业务我觉得行为模式更多一些,搭配别的模式花样更多。
快照或者说备忘录模式的代码实现比较灵活(就是满足要求满足定义就可以,不强调实现的结构),应用场景也比较明确和有限,主要是用来防丢失、撤销、恢复等。
GoF的备忘录模式定义: Memento Design Pattern
Captures and externalizes an object’s internal state so that it can be
restored later, all without violating encapsulation.
在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
涉及到两个问题:
为什么存储和恢复副本会违背封装原则?
备忘录模式是如何做到不违背封装原则的?
有一个例子可以对应上面的问题
假设有这样一道面试题,希望你编写一个小程序,可以接收命令行的输入。用户输入文本时,程序将其追加存储在内存文本中;用户输入“:list”,程序在命令行中输出内存文本的内容;用户输入“:undo”,程序会撤销上一次输入的文本,也就是从内存文本中将上次输入的文本删除掉。
结果大概是这样的
>hello
>:list
hello
>world
>:list
helloworld
>:undo
>:list
hello
第一种存储和恢复副本的形式
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 类来表示快照违背了封装原则。
备忘录实现
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);
}
}
}
}
我这里是一个我自己添加了一个退出机制,就是:exit的代码改写。
快照模式演示demo
相当于是创建一个快照类来存快照,和inputext做一个解耦,inputtext依旧负责字符串的操作和保存,只不过这个备份由snapshot。现实中我们很多设计会涉及到备份的概念,但是考虑到备份太多占用空间以及IO,基本是低频全量备份,高频增量备份来解决。