軟體設計

Swift Memento Pattern 完整整理(復原實作範例)

你是否曾在 iOS App 製作「復原」按鈕時遇到瓶頸?

閱讀 4 分鐘
Swift Memento Pattern 完整整理(復原實作範例) 封面圖

你是否曾在 iOS App 製作「復原」按鈕時遇到瓶頸?

在文字編輯器 App 中,若要讓使用者復原剛才的編輯,首先就得思考如何保留先前的狀態。

如果在各處備份物件內部的屬性,程式碼很快就會變得凌亂。

這時最適合的就是Swift Memento Pattern。今天會搭配程式碼,分享我實際使用這個模式優雅儲存與還原物件狀態的經驗。

Memento Pattern 是一種設計模式,能在不破壞封裝的情況下,將物件的內部狀態儲存到外部,之後再還原成該狀態。

一句話說,就是「先拍下快照」。復原(Undo)、遊戲存檔、表單暫存等功能都建立在這個概念上。


什麼是 Memento Pattern?先從三種角色開始 🧩

Memento Pattern 的角色剛好有三個。理解這裡,就完成一半了。

  • Originator(原始物件):持有狀態的主角。將自己的狀態儲存成 Memento,也能透過 Memento 還原。
  • Memento(備忘錄):包含特定時間點狀態的快照。建立後內容最好不要再變更。
  • Caretaker(管理者):負責保管 Memento。通常就是復原堆疊。

重點在於,管理者不會查看 Memento「內部」的內容。

管理者只會接收快照並堆疊保存,需要時再交還。原始物件的內部結構只有原始物件自己知道。

因此不會破壞封裝,這正是 Memento Pattern 最大的魅力。


如何使用 Swift 實作 Memento Pattern?

以簡單的文字編輯器為例。先定義 Memento 與原始物件。

下面是保存狀態的 Memento,以及建立並還原該 Memento 的原始物件。

// 狀態快照 — 建立後保持不變 let
struct EditorMemento {
    let text: String
}

final class TextEditor {
    var text: String = ""
    func save() -> EditorMemento { EditorMemento(text: text) }   // 儲存
    func restore(_ memento: EditorMemento) { text = memento.text } // 還原
}

save()會將目前狀態拍成快照,restore(_:)則會還原成收到的快照。

重點是將EditorMemento的屬性設為let,避免儲存當下的狀態之後遭到修改。

只要分開 save 與 restore,程式碼就能變得這麼簡潔
只要分開 save 與 restore,程式碼就能變得這麼簡潔

實戰:製作復原(Undo)功能 ⏪

接著加入管理者,也就是復原堆疊,負責逐一保存前面建立的 Memento。

final class UndoManager {
    private var history: [EditorMemento] = []
    func backup(_ memento: EditorMemento) { history.append(memento) }
    func undo() -> EditorMemento? { history.popLast() } // 取出最近的狀態
}

實際流程如下。

把儲存與還原流程畫成圖,就是這樣
把儲存與還原流程畫成圖,就是這樣

使用者修改文字前,先透過backup儲存目前狀態。

按下「復原」時,undo會取出最後一個快照,原始物件再使用它來restore。

實際接上後,編輯邏輯與儲存邏輯清楚分開,程式碼簡潔了不少。

即使混合了新增文字、刪除文字、變更格式等多種編輯,儲存方式仍能統一,維護起來很方便。

一個復原按鈕就能在這個模式上運作
一個復原按鈕就能在這個模式上運作

Swift 風格的使用技巧:活用 Codable 與 struct

許多介紹 Memento Pattern 的文章,原本都是以 Java 等語言為前提。不過在 Swift 中善用語言特性,實作會方便許多。

第一,Memento 比起class更適合使用struct。因為是值型別,複製的瞬間就會成為獨立的快照,也能減少參考互相牽連而導致原始狀態被修改的疑慮。

第二,讓 Memento 採用Codable後,就容易將狀態儲存到檔案或 UserDefaults,也能擴充成關閉並重新開啟 App 後仍保留的存檔功能。

第三,如果使用 UIKit,建議先了解現成的UndoManager系統類別。自行實作前,先掌握標準工具才是正確順序。


常見問題(Q&A)

Q. 直接備份到變數裡不行嗎?

簡單的情況可以。但當狀態變多,或儲存/還原邏輯變複雜時,散落各處的備份程式碼就會成為 Bug 的溫床。Memento Pattern 能將它集中在同一處。

Q. 不會使用太多記憶體嗎?

沒錯。無限堆疊快照會增加記憶體負擔。因此實務上常將歷史記錄限制在約 20~50 筆,或只儲存有變更的部分來最佳化。


Memento Pattern 只是名稱陌生,實際上就是「拍下快照並還原」這個簡單概念。

如果你正在考慮復原或暫存功能,不妨先跟著今天的範例程式碼實作一次。你會很快掌握在維持封裝的同時處理狀態的感覺。加油!😊

延伸閱讀