设计模式:备忘录模式

备忘录模式

备忘录模式,也叫快照(Snapshot)模式,英文翻译是 Memento Design Pattern,应用场景比较明确和有限,主要是用来防丢失、撤销、恢复等。定义为:在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态

这个模式的定义主要表达了两部分内容:

  • 第一部分:存储副本以便后期恢复
  • 第二部分:要在不违背封装原则的前提下,进行对象的备份和恢复

问题是:

  • 为什么存储和恢复副本会违背封装原则?
  • 备忘录模式是如何做到不违背封装原则的?

例子

假设有这样一道面试题,希望你编写一个小程序,可以接收命令行的输入。用户输入文本时,程序将其追加存储在内存文本中;用户输入“: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.toString());
			} 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 类对象存储的文本来做撤销操作。

这里再举一个例子。假设每当有数据改动,我们都需要生成一个备份,以备之后恢复。如果需要备份的数据很大,这样高频率的备份,不管是对存储(内存或者硬盘)的消耗,还是对时间的消耗,都可能是无法接受的。想要解决这个问题,我们一般会采用“低频率全量备份”和“高频率增量备份”相结合的方法。

所谓的“全量备份”就是把所有的数据“拍个快照”保存下来,所谓的“增量备份”,指的是记录每次操作或数据变动。

当我们需要恢复到某一时间点的备份的时候,如果这一时间点有全量备份,我们直接拿来恢复就可以了。如果这一时间点没有对应的全量备份,我们就找到最近一次全量备份,然后用它来恢复,之后执行此次全量备份跟这一时间点之间的所有增量备份,也就是对应的操作或者数据变动,这样就能减少全量备份的数量和频率,减少对时间、内存的消耗。

总结

备忘录模式也叫快照模式,具体来说,就是在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。这个模式的定义表达了两部分内容:一部分是,存储副本以便后期恢复;另一部分是,要在不违背封装原则的前提下,进行对象的备份和恢复。

备忘录模式的应用场景也比较明确和有限,主要是用来防丢失、撤销、恢复等。它跟平时我们常说的“备份”很相似。两者的主要区别在于,备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计。

对于大对象的备份来说,备份占用的存储空间会比较大,备份和恢复的耗时会比较长。针对这个问题,不同的业务场景有不同的处理方式。比如,只备份必要的恢复信息,结合最新的数据来恢复;再比如,全量备份和增量备份相结合,低频全量备份,高频增量备份,两者结合来做恢复。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值