之前我们学习了访问者模式,在23种设计模式中,访问者模式的原理比较难以理解,还有讲解了Single Dispatch,Double Dispatch这两者的思路不好理解,而今天我们讲解一下备忘录模式,理解起来并不难,其主要是为了防止丢失,撤销,恢复的,学起来相对的容易

备忘录模式的原理

在设计模式中,定义为了 在不违背封装原则的前提下,捕获一个对象的内部状态,并且在对象外部保存这个状态,方便恢复的到之前的状态

这个模式的解释体现了两个部分,一个是存储了副本方便后期的恢复,另一个是在不违背封装原则的前提下,进行备份和恢复

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

如何避免违背封装原则的 ?

假设有如下的场景.编写一个小程序,接收到用户的输入,在输入:list时候,输出内存文本的内容,输入 :undo的时候,撤销上一次输入的文本,也就是内存文本中将上一次输入的文本删除掉

图片

怎么来实现呢?可以打开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.toString());

} else if (input.equals(“:undo”)) {

InputText snapshot = snapshotsHolder.popSnapshot();

inputText.setText(snapshot.getText());

} else {

snapshotsHolder.pushSnapshot(inputText);

inputText.append(input);

}

}

}

}

实际上,备忘录模式的实现很灵活,也没有固定的实现方式,但是我们违背了封装的原则

1.因为为了能够快速的恢复InputText对象,定义了setText()函数,但是这个函数可能被其他的业务的使用,暴露了不应该暴露的函数,违背了封装原则

2.快照本身是不可变的,理论上,不应该包含任何set()等修改的内部状态的函数,但是在上面的实现中,我们复用了InputText类的定义,修改了内部状态

于是,为了保护封装特性,我们进行了重构,将setText()方法重命名为restoreSnapshot()方法,

并且独立出一个列Snapshot类来表示快照,而不是简单的复用InputText类,我们在整体的栈中压入压出的就是这个Snapshot类

代码重构如下

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);

}

}

}

}

这就是备忘录模式的代码实现,也就是很多书籍中的实现方式

但是在备忘录模式之外,更为常见的是备份,和备忘录很类似,不过备忘录更加侧重于代码的设计和实现,备份更加侧重于架构的设计和产品设计

而且在备忘录的基础上,我们还会有性能上的优化

如果备份的对象大,而且频率高,如何解决?

1.如果还是如上的场景,支持顺序撤销,而且一次只能撤销一次,那么我们没有必要保存全部的文本

只需要保存一个偏移量即可,然后在回滚的时候,结合InputText类来进行撤销操作

2.如果每次数据的变动,我们都需要生成备份,不如改为增量备份,使用低频率的全量备份和高频率的增量备份相结合的方法

增量备份就是每次操作的变动,全量备份就是讲整个数据进行一次保存

当我们需要恢复的时候,先找到最近一次的全量备份,然后进行恢复,然后一次使用增量备份,进行对应的操作,减少对时间和内存的消耗

本章的重点

备忘录模式也叫做快照模式,在不违背原则的前提下,捕获一个对象的内部状态,然后保存在对象的外部,在需要的时候,利用外部的保存进行相对应的恢复

需要注意的是,需要在不违背的封装原则的前提下,进行对象的备份和恢复

备份和备忘录很相似,都是为了防止丢失 撤销的恢复工作,但是备忘录模式更加侧重于代码的设计和实现,备份更加侧重于架构的设计和实现

在备忘录模式的基础上我们会进行相对应的优化

对于大对象的备份,而且频率大的,我们可以进行不同的处理方式,比如,只备份必要的恢复

或者使用,全量备份加增量备份结合,低频全量备份,高配增量备份,两者结合恢复

课后思考

备份在架构中很常见,举几个列子

1.从开发上将,在使用定时任务Quartz的时候,会进行对应的备份,方便我们在项目重启后从数据库中反序列化回来,利用了一个外部工具来进行了备份

2.在整体架构中,MySQL就是使用全量备份和增量备份相结合的方式进行了备份,我们自己的项目也是一星期一次全量,配合binlog回滚

3.在生活中,我记得XBox上的极限竞速游戏提供了回滚功能,就是使用的备份来方便撞车后直接回溯操作

发表评论

邮箱地址不会被公开。 必填项已用*标注