之前我们学习了访问者模式,在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上的极限竞速游戏提供了回滚功能,就是使用的备份来方便撞车后直接回溯操作