你是否曾在嘗試自行製作復原功能時卡住?
這是按一下 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 模式 | Memento 模式 |
|---|---|---|
| 儲存對象 | 操作(execute/undo) | 狀態快照 |
| 記憶體 | 輕量 | 狀態大時較沉重 |
| 復原邏輯 | 針對每個操作自行實作 | 還原即可 |
| redo | 透過堆疊回退自然實作 | 可使用快照陣列實作 |
| 適合的場景 | 編輯器、繪圖工具 | 遊戲存檔、表單狀態 |
用一句話總結如下。
如果能清楚設計還原操作,就選 Command;如果完整保存狀態比較省心,就選 Memento。
何時使用,何時避開
實際選擇時,我會依照以下標準判斷。
- 操作單位明確,且需要 redo → Command。編輯器或繪圖工具就是代表案例。
- 狀態小,複製快照不會造成負擔 → Memento。適合表單輸入、還原設定值等場景。
- 狀態龐大,但變更範圍很局部 → 請避開 Memento。每次完整複製都可能讓記憶體爆掉,Command 會更適合。
- 完全想不到還原邏輯 → 不要勉強使用 Command,用 Memento 儲存快照會更乾淨。
實務上也會混用兩者:用 Command 管理操作,只針對難以還原的部分以 Memento 留下快照。
面試時會這樣問
Q. Command 模式和 Memento 模式有什麼差異?
Command 將「操作」封裝成物件,並讓 execute 與 undo 成對存在。Memento 則將物件的「狀態」儲存為快照,再進行還原。也就是說,差別在於透過操作的反向運算,還是透過狀態還原來復原。
Q. 如果要實作 undo 堆疊,你會選哪一種?
如果操作單位明確,且希望自然支援 redo,就選 Command。反之,若狀態很小,撰寫還原邏輯又很麻煩,使用 Memento 堆疊快照會更簡單安全。
建議你親自將兩種模式各實作一次。只閱讀概念,和親眼看到物件堆疊、狀態因 Ctrl+Z 還原,理解深度完全不同。希望今天的文章能幫助你踏出第一步。

