软件设计

Command(命令)vs Memento(备忘录):实现撤销的两种方法总结

你是否曾在尝试自己实现撤销功能时卡住?

4 分钟阅读
Command(命令)vs Memento(备忘录):实现撤销的两种方法总结 封面图

你是否曾在尝试自己实现撤销功能时卡住?

这是按一次 Ctrl+Z 就能完成的常见功能,但真正写进代码后却相当棘手。即使是类似画图软件的玩具项目,也可能让你花上好几天。

先说结论:撤销的实现大致分为两种思路。

一种是通过 Command 模式反向撤销已执行的操作,另一种是通过 Memento 模式完整保存操作前的状态。

两种都正确,只是适用时机不同。今天我们结合代码比较这两种方式。


Command 模式 — 将操作封装为对象

Command 模式会把“做了什么”放进一个对象里。

可以把它理解为一个将 execute 和 undo 配对的对象。

例如,如果操作是输入文字,就提前在同一个对象中定义它的反向操作:“删除输入的内容”。

protocol Command {
    func execute()
    func undo()
}

struct TypeCommand: Command {
    let text: String
    let editor: Editor
    func execute() { editor.content += text }
    func undo() { editor.content.removeLast(text.count) }
}

然后把创建好的 Command 对象逐个压入栈中。

当收到 Ctrl+Z 时,取出栈顶对象并调用 undo() 就完成了。

Command 模式保存的不是状态,而是“操作”。


Memento 模式 — 将状态保存为快照

Memento 采用完全不同的思路。

与其思考如何撤销每个操作,不如直接保存变更前的完整状态,就像拍照一样。

struct Memento { let content: String }

class Editor {
    var content = ""
    func save() -> Memento { Memento(content: content) }
    func restore(_ m: Memento) { content = m.content }
}

let editor = Editor()
editor.content = "你好"
let snapshot = editor.save()   // 在这里保存快照
editor.content = "你好"
editor.restore(snapshot)
print(editor.content)
// 输出:你好

需要撤销时,用保存的快照替换当前状态即可。就这么简单。

不需要为每个操作分别编写撤销逻辑,确实省心。

但缺点也很明显。如果状态很大,每次都要完整复制,内存消耗会相当可观。

将 Command 对象压入栈中的结构示意图
将 Command 对象压入栈中的结构示意图

那么,应该使用哪一种?

我把实际使用两者后感受到的差异整理成了表格。

标准 Command 模式 Memento 模式
存储对象 操作(execute/undo) 状态快照
内存 轻量 状态较大时开销较高
撤销逻辑 针对每个操作手动实现 只需恢复状态
redo 通过出栈自然实现 可以使用快照数组
适合场景 编辑器、图形工具 游戏存档、表单状态

用一句话总结就是:

如果能明确编写撤销操作,就选 Command;如果完整保存状态更省心,就选 Memento。

那张曾让我对着一个 Ctrl+Z 苦战好几天的桌子
那张曾让我对着一个 Ctrl+Z 苦战好几天的桌子

何时使用,何时避免

实际选择时,我会参考以下标准。

  • 操作边界清晰,并且还需要 redo → Command。编辑器和绘图工具就是典型例子。
  • 状态较小,复制快照不会造成负担 → Memento。适合表单输入、恢复配置值等场景。
  • 状态非常庞大,但改动范围很小 → 避免使用 Memento。每次完整复制都可能耗尽内存,Command 更合适。
  • 完全想不出撤销逻辑 → 不要强行使用 Command,用 Memento 保存快照更简洁。

实际工作中也可以混用两者:用 Command 管理操作,只对难以撤销的部分使用 Memento 保存快照。


面试中会这样问

Q. Command 模式和 Memento 模式有什么区别?

Command 将“操作”封装为对象,并将 execute 与 undo 配对。Memento 则把对象的“状态”保存为快照,之后再恢复。也就是说,区别在于通过操作的逆运算撤销,还是通过恢复状态撤销。

Q. 如果要实现撤销栈,你会选择哪一种?

如果操作边界清晰,并且希望自然支持 redo,就选择 Command。相反,如果状态较小,而编写撤销逻辑比较麻烦,用 Memento 堆叠快照会更简单、更安全。


建议你亲自把两种模式各实现一次。只阅读概念,和亲眼看到对象压入栈中、状态通过 Ctrl+Z 回退,理解深度完全不同。希望今天的文章能帮助你迈出第一步。

延伸阅读