重修设计模式-行为型-备忘录模式
重修设计模式-行为型-备忘录模式
Captures and externalizes an object’s internal state so that it can be restored later, all without violating encapsulation.
在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
备忘录模式(Memento Pattern)允许在不暴露对象实现细节的情况下保存和恢复对象的状态。该模式的主要目的是通过捕获和存储对象的内部状态,以便将来能够将对象恢复到这个状态。
备忘录模式包含三个主要角色:
-
发起者(Originator)
:负责创建一个备忘录,并且可以记录、恢复自身的内部状态。可以根据需要决定 Memento 保存自身的那些内部状态。 -
备忘录(Memento)
:存储 Originator 内部状态的快照,防止 Originator 以外的的对象访问 Memento。 -
管理者(Caretaker)
:负责存储备忘录,但不能检查或操作备忘录的内容。
举个例子,现在需要完成一个文本编辑器,在输入文本时,程序需要将内容追加存储在内存文本中,在输入:undo
命令后,会执行撤销操作,该操作会将保存的文本内容撤销到上一次输入状态。
对于该需求,首先需要创建的是编辑器类(Editor),这是编辑器需求的承载类。
class Editor {
private val text: StringBuilder = StringBuilder()
fun append(input: String) {
this.text.append(input)
}
fun getText(): String = text.toString()
}
Editor 类只对外暴露 append 和 getText 方法,用于拼接内容和获取内容,符合迪米特法则,不该暴露的内部细节就不暴露。
其次需要考虑备份和恢复问题,回到备忘录模式,Editor 其实相当于 Originator
角色,这时还需要创建 Memento
角色来存储 Originator
的内部状态快照。这里需要思考两个问题:
1.能否直接复用 Editor 类作为快照的内部状态?
//如:
class Snapshot(private var editor: Editor) {
fun getEditor() = editor
}
//恢复时:Editor暴露set方法,便于从快照恢复状态
class Editor {
//
fun setText(editor: Editor) {
this.text.replace(0, this.text.length, editor.getText())
}
...
}
直接复用 Editor 类作为内部状态,创建快照时通过深拷贝实现,好处是 Editor 需要备份的属性不用重写一份,不过缺点也显而易见:
- 第一,为了能用快照恢复 Editor 对象,需要在 Editor 内部定义 setText() 函数,但这个函数是公用的,可能会被其他业务使用,暴露不应该暴露的函数,违背封装原则。
- 第二,快照本身是不可变的,但上面的快照复用了业务模型 Editor 类的定义,而 Editor 类本身有一系列修改内部状态的函数,同样违背封装原则。
- 第三,复用 Editor 意味着无法灵活选择要备份的属性,即使通过深浅拷贝的实现来达到目的(需要备份的深拷贝,不需要备份的浅拷贝或不拷贝),也会存在数据冗余问题。
2.在哪里进行快照的生成和恢复?
快照的创建和恢复方法只能在 Originator
角色内部定义,如果在 Originator 外部,就需要暴露类的所有内部细节而使其过于脆弱;如果对外屏蔽类的内部细节又无法生成快照,所以快照的生成和恢复只能由 Originator 自身来实现。这也是备忘录模式“不暴露对象实现细节的情况下,保存和恢复对象的状态”的核心。
Caretaker
角色的定义非常简答, 这里创建 SnapshotHolder 类,并在内部通过栈结构来存储历史快照。
上面的例子使用备忘录模式实现的完整代码如下:
//Originator:文本编辑器
class Editor {
private val text: StringBuilder = StringBuilder()
fun append(input: String) {
this.text.append(input)
}
fun getText(): String = text.toString()
//--生成快照--
fun createSnapshot(): Snapshot {
return Snapshot(text.toString())
}
//--从快照恢复--
fun restoreSnapshot(snapshot: Snapshot) {
this.text.replace(0, this.text.length, snapshot.getText())
}
}
//Memento:快照
class Snapshot(private val text: String) {
fun getText() = text
}
//Caretaker:存储快照
class SnapshotHolder {
private val snapshots: Stack<Snapshot> = Stack()
fun popSnapshot(): Snapshot? {
return if (snapshots.isEmpty()) null else snapshots.pop()
}
fun pushSnapshot(snapshot: Snapshot) {
snapshots.push(snapshot)
}
}
fun main() {
println("please input:")
val editor = Editor()
val snapshotHolder = SnapshotHolder()
val scanner = Scanner(System.`in`)
while (scanner.hasNext()) {
val input: String = scanner.next()
if (input == ":undo") {
snapshotHolder.popSnapshot()?.let { snapshot ->
editor.restoreSnapshot(snapshot) //从快照恢复上一状态
} ?: println("无存储的快照了!")
} else {
//在发起者内部状态发生变化之前,创建快照对象,将内部状态保存。
snapshotHolder.pushSnapshot(editor.createSnapshot())
editor.append(input)
}
println("current content:${editor.getText()}")
}
}
代码执行结果:
如何优化内存和时间消耗?
对于大对象的备份来说,备份占用的存储空间会比较大,备份和恢复的耗时会比较长。针对这个问题,不同的业务场景有不同的处理方式。比如,只备份必要的恢复信息,结合最新的数据来恢复;再比如,全量备份和增量备份相结合,低频全量备份,高频增量备份,两者结合来做恢复,具体思路如下:
当需要恢复到某一时间点的备份的时候,如果这一时间点有做全量备份,直接拿来恢复即可。如果这一时间点没有对应的全量备份,就先找到最近的一次全量备份,然后用它来恢复,之后执行此次全量备份跟这一时间点之间的所有增量备份,也就是对应的操作或者数据变动。这样就能减少全量备份的数量和频率,减少对时间、内存的消耗。
总结
备忘录模式通过封装对象的内部状态,提供了一种灵活且安全的状态管理机制,非常适合用于需要保存和恢复状态的复杂系统中,比如在需要实现撤销操作或保存和恢复对象状态的场景,例如文本编辑器、游戏状态保存等。