意圖(Intent)#
在不違反封裝的前提下,捕捉並外部化一個物件的內部狀態,使該物件之後可以恢復到此狀態。
別名(Also Known As)#
Token
動機(Motivation)#
實作 checkpoint 與 undo 機制時,需要儲存物件的內部狀態以便日後還原。但物件通常將狀態封裝在內部,外部無法存取——若公開這些狀態又會破壞封裝性,影響應用程式的可靠性與可擴展性。
以圖形編輯器為例:編輯器使用 ConstraintSolver 維護物件之間的連接關係。當使用者移動矩形時,單純反向移動並不一定能恢復到先前的狀態(例如連接線存在鬆弛度時)。undo 機制需要與 ConstraintSolver 密切合作來還原狀態,但又不應暴露其內部實作。
Memento 的解法:
- Memento 物件儲存另一個物件(originator)的內部狀態快照
- 只有 originator 可以存取 memento 中的資訊——對其他物件來說 memento 是不透明的(opaque)
- Undo 流程:editor 在操作前向 ConstraintSolver 請求 memento -> 執行操作 -> 需要 undo 時將 memento 還給 ConstraintSolver -> ConstraintSolver 據此恢復狀態
Memento 的關鍵設計是雙重介面:Caretaker 只能看到窄介面(只能傳遞 memento),Originator 能看到寬介面(可存取所有恢復狀態所需的資料)。這確保了在不破壞封裝的前提下儲存與恢復狀態。
適用場景(Applicability)#
- 需要儲存物件的(部分)狀態快照,以便日後恢復
- 直接公開取得狀態的介面會暴露實作細節、破壞封裝
結構(Structure)#
Originator 建立包含其內部狀態快照的 Memento,並在需要時使用 memento 恢復狀態。Caretaker(如 undo 機制)負責保管 memento,但絕不操作或檢查其內容。
classDiagram
class Originator {
-state
+SetMemento(Memento)
+CreateMemento()
}
class Memento {
-state
+GetState()
+SetState()
}
class Caretaker
Originator ..> Memento : creates
Caretaker o--> Memento參與者(Participants)#
| 參與者 | 範例 | 職責 |
|---|---|---|
| Memento | SolverState | 儲存 Originator 的內部狀態(儲存多少由 originator 決定);提供雙重介面:Caretaker 只見窄介面(僅能傳遞),Originator 見寬介面(可存取所有資料) |
| Originator | ConstraintSolver | 建立包含當前狀態快照的 memento;使用 memento 恢復內部狀態 |
| Caretaker | undo 機制 | 負責保管 memento;不操作或檢查 memento 的內容 |
協作方式(Collaborations)#
- Caretaker 向 Originator 請求 memento,持有一段時間後歸還(若需要恢復狀態)
- Memento 是被動的——只有建立它的 Originator 可以賦值或取出其狀態
優缺點(Consequences)#
- 維護封裝邊界——Originator 內部狀態不必為了儲存而暴露給外部,封裝完整保留
- 簡化 Originator——狀態管理的負擔轉移給客戶端(caretaker),Originator 不需要自己維護歷史版本
- 可能成本高昂——若 Originator 需要複製大量資訊,或 memento 頻繁建立與歸還,開銷可能可觀
- 窄寬介面的實作困難——某些語言難以確保只有 Originator 能存取 memento 的狀態(C++ 可用
friend機制解決) - 隱藏的保管成本——Caretaker 不知道 memento 中儲存了多少狀態,一個輕量的 caretaker 可能因儲存大量 memento 而消耗大量記憶體
Caretaker 無法得知 memento 的實際大小。若不加控制地累積 memento(例如無限的 undo history),可能導致嚴重的記憶體問題。應設定 history list 的上限或使用增量儲存策略。
實作要點(Implementation)#
- 語言支援——理想情況下需要兩層靜態保護。C++ 做法:將 Originator 設為 Memento 的
friend,將 Memento 的寬介面設為private,只公開窄介面 - 儲存增量變更(incremental changes)——當 memento 以可預測的順序建立與歸還時,可以只儲存 Originator 狀態的增量變更而非完整快照,大幅降低儲存成本。例如在 undo 機制中,history list 定義了確定的 undo/redo 順序,memento 只需儲存每個命令造成的差異
增量儲存(incremental memento)是降低 Memento 成本的關鍵技巧。若命令的執行順序是確定的(如搭配 Command 模式的 history list),每個 memento 只需記錄「這個命令改了什麼」,而非「整個世界的狀態」。
已知應用(Known Uses)#
- Unidraw 的
CSolver類別使用 memento 來支援圖形連接的 undo - Dylan 的集合框架使用「state object」作為迭代的 memento,集合可自由選擇如何表示迭代狀態,表示方式完全隱藏。相比讓 iterator 成為集合的
friend,memento-based 的做法反過來讓集合成為 state 的 friend,更好地保護了封裝 - QOCA 約束求解工具包儲存增量式 memento——只記錄自上次求解以來變化的約束變數
相關模式(Related Patterns)#
- Command:命令可使用 memento 來維護可復原操作所需的狀態
- Iterator:memento 可用於迭代,如前述 Dylan 的做法