意圖(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)#

參與者範例職責
MementoSolverState儲存 Originator 的內部狀態(儲存多少由 originator 決定);提供雙重介面:Caretaker 只見窄介面(僅能傳遞),Originator 見寬介面(可存取所有資料)
OriginatorConstraintSolver建立包含當前狀態快照的 memento;使用 memento 恢復內部狀態
Caretakerundo 機制負責保管 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)#

  • UnidrawCSolver 類別使用 memento 來支援圖形連接的 undo
  • Dylan 的集合框架使用「state object」作為迭代的 memento,集合可自由選擇如何表示迭代狀態,表示方式完全隱藏。相比讓 iterator 成為集合的 friend,memento-based 的做法反過來讓集合成為 state 的 friend,更好地保護了封裝
  • QOCA 約束求解工具包儲存增量式 memento——只記錄自上次求解以來變化的約束變數

相關模式(Related Patterns)#

  • Command:命令可使用 memento 來維護可復原操作所需的狀態
  • Iterator:memento 可用於迭代,如前述 Dylan 的做法