設計模式(十九)行為型:備忘錄模式詳解
備忘錄模式(Memento Pattern)是 GoF 23 種設計模式中的行為型模式之一,其核心價值在于在不破壞封裝性的前提下,捕獲并外部化一個對象的內部狀態,以便在之后能夠將該對象恢復到原先保存的狀態。它通過引入“備忘錄”對象來存儲原發器(Originator)的快照,由管理者(Caretaker)負責保存和管理這些快照,從而實現狀態的保存與恢復。備忘錄模式是實現撤銷(Undo)、重做(Redo)、事務回滾、游戲存檔、對象版本控制、配置快照等關鍵功能的基礎,是構建具備“時間旅行”能力、高容錯性和用戶友好性的系統的必備設計模式。
一、詳細介紹
備忘錄模式解決的是“需要保存對象在某一時刻的狀態,并在需要時恢復該狀態,但又不能暴露對象的內部實現細節”的問題。在傳統設計中,若要實現撤銷功能,可能會讓對象提供 getState()
和 setState()
方法,但這會破壞對象的封裝性,暴露其內部結構,導致耦合度高、安全性差。
備忘錄模式的核心思想是:將狀態的保存與恢復過程封裝在三個角色中,通過“窄接口”與“寬接口”的設計,既保證了封裝性,又實現了狀態管理。
該模式包含以下核心角色:
- Originator(原發器):創建一個備忘錄來記錄當前內部狀態,并可在需要時根據備忘錄恢復狀態。它負責定義如何保存和恢復自身狀態。
- Memento(備忘錄):存儲原發器的內部狀態。備忘錄對象的接口設計是關鍵:
- 窄接口(Narrow Interface):對外(管理者)只暴露最小必要接口,通常無任何方法(或僅用于標識),防止管理者修改狀態。
- 寬接口(Wide Interface):對原發器暴露完整狀態訪問權限,允許原發器讀取狀態以恢復自身。
在 Java 中,通常通過內部類或包級私有類實現,利用訪問控制機制實現接口隔離。
- Caretaker(管理者):負責保存和管理備忘錄對象,但不能對備忘錄的內容進行任何操作或檢查。它只是“保管”備忘錄,并在需要時將其交還給原發器。
備忘錄模式的關鍵優勢:
- 保持封裝性:原發器的內部狀態通過備忘錄安全地外部化,管理者無法訪問或修改。
- 支持撤銷/重做:通過保存多個備忘錄(如棧結構),可實現多級撤銷與重做。
- 實現事務回滾:在操作失敗時,可恢復到操作前的狀態。
- 支持版本控制:可保存對象的歷史狀態快照。
- 解耦狀態管理:狀態的保存與恢復邏輯由原發器控制,管理者僅負責存儲。
與“命令模式”相比,命令封裝操作,備忘錄封裝狀態;命令常與備忘錄結合實現撤銷功能(命令執行前保存狀態)。與“觀察者模式”相比,觀察者關注狀態變化的通知,備忘錄關注狀態的保存與恢復。
備忘錄模式適用于:
- 需要實現撤銷/重做功能(如文本編輯器、圖形編輯器)。
- 需要事務性操作或回滾機制。
- 需要保存游戲進度或配置快照。
- 需要對象狀態的歷史版本管理。
二、備忘錄模式的UML表示
以下是備忘錄模式的標準 UML 類圖:
圖解說明:
Originator
創建Memento
保存狀態,并可從Memento
恢復狀態。Memento
存儲Originator
的狀態。Caretaker
保存Memento
對象,但無法訪問其內容。- 實際實現中,
Memento
的getState()
方法通常設為private
或包私有,僅Originator
可訪問(通過內部類或友元機制)。
三、一個簡單的Java程序實例及其UML圖
以下是一個文本編輯器的示例,支持保存當前文本狀態并撤銷到之前的狀態。
Java 程序實例
import java.util.ArrayList;
import java.util.List;// 備忘錄類:存儲編輯器狀態
// 使用包級私有類實現接口隔離
class EditorMemento {private final String content;private final long timestamp;// 包級私有構造函數,僅 Originator 可創建EditorMemento(String content) {this.content = content;this.timestamp = System.currentTimeMillis();}// 包級私有方法,僅 Originator 可讀取狀態String getContent() {return content;}long getTimestamp() {return timestamp;}@Overridepublic String toString() {return "Memento@" + timestamp + "[content='" + content + "']";}
}// 原發器:文本編輯器
class TextEditor {private String content = "";public void type(String text) {this.content += text;System.out.println("📝 輸入: \"" + text + "\"");System.out.println("📄 當前內容: \"" + content + "\"");}public String getContent() {return content;}// 創建備忘錄(保存當前狀態)public EditorMemento save() {System.out.println("💾 保存當前狀態到備忘錄");return new EditorMemento(content);}// 從備忘錄恢復狀態public void restore(EditorMemento memento) {this.content = memento.getContent();System.out.println("? 恢復到備忘錄狀態: " + memento);System.out.println("📄 恢復后內容: \"" + content + "\"");}
}// 管理者:歷史記錄
class History {private List<EditorMemento> mementos = new ArrayList<>();// 保存備忘錄public void push(EditorMemento memento) {mementos.add(memento);System.out.println("📦 管理者保存備忘錄,歷史記錄數量: " + mementos.size());}// 獲取備忘錄(通常按棧或隊列順序)public EditorMemento pop() {if (mementos.isEmpty()) {System.out.println("?? 無可用備忘錄");return null;}EditorMemento memento = mementos.remove(mementos.size() - 1);System.out.println("📤 管理者提供備忘錄,剩余數量: " + mementos.size());return memento;}public boolean isEmpty() {return mementos.isEmpty();}
}// 客戶端使用示例
public class MementoPatternDemo {public static void main(String[] args) {System.out.println("📝 文本編輯器 - 備忘錄模式示例\n");// 創建原發器和管理者TextEditor editor = new TextEditor();History history = new History();// 初始狀態System.out.println("--- 初始狀態 ---");editor.type("Hello");// 保存狀態1System.out.println("\n--- 保存狀態1 ---");history.push(editor.save());// 修改狀態System.out.println("\n--- 修改狀態 ---");editor.type(" World");// 保存狀態2System.out.println("\n--- 保存狀態2 ---");history.push(editor.save());// 再次修改System.out.println("\n--- 再次修改 ---");editor.type("! How are you?");// 撤銷(恢復到狀態2)System.out.println("\n--- 執行撤銷 (Undo) ---");EditorMemento memento = history.pop();if (memento != null) {editor.restore(memento);}// 再次撤銷(恢復到狀態1)System.out.println("\n--- 再次撤銷 (Undo) ---");memento = history.pop();if (memento != null) {editor.restore(memento);}// 嘗試撤銷(無更多狀態)System.out.println("\n--- 再次撤銷 ---");memento = history.pop(); // 應為空// 演示:管理者無法訪問備忘錄內容System.out.println("\n💡 說明:管理者 (History) 無法讀取備忘錄內容,");System.out.println("🔒 保證了封裝性。備忘錄的 getContent() 方法是包私有。");}
}
實例對應的UML圖(簡化版)
運行說明:
TextEditor
是原發器,維護文本內容。EditorMemento
是備忘錄,存儲文本快照和時間戳。History
是管理者,使用棧結構保存備忘錄,實現撤銷功能。- 客戶端通過
save()
創建備忘錄并交由History
保存。 - 通過
pop()
獲取備忘錄并調用restore()
恢復狀態。 EditorMemento
的getContent()
為包私有,僅TextEditor
可訪問,History
無法讀取內容,保證封裝性。
四、總結
特性 | 說明 |
---|---|
核心目的 | 安全保存并恢復對象狀態,保持封裝性 |
實現機制 | 原發器創建備忘錄,管理者存儲,原發器恢復 |
優點 | 保持封裝性、支持撤銷/重做、實現回滾、解耦狀態管理 |
缺點 | 可能消耗大量內存(保存多個快照)、管理復雜狀態時備忘錄設計復雜 |
適用場景 | 撤銷/重做、事務回滾、游戲存檔、配置快照、版本控制 |
不適用場景 | 狀態極小或無需保存、性能極度敏感、狀態頻繁變化 |
備忘錄模式使用建議:
- 使用窄接口設計,防止管理者訪問內部狀態。
- 考慮內存開銷,可實現快照壓縮、增量保存或限制歷史記錄長度。
- 可結合命令模式,命令對象在執行前保存備忘錄。
- 在 Java 中,可使用內部類實現備忘錄,天然支持訪問控制。
架構師洞見:
備忘錄模式是“狀態持久化”與“時間控制”的抽象。在現代架構中,其思想已演變為事件溯源(Event Sourcing)、CQRS(命令查詢職責分離)、數據庫事務日志 和 分布式快照 的核心。例如,在事件溯源中,對象狀態由事件流重建,每個事件相當于一個“操作備忘錄”;在分布式系統中,Raft 或 Paxos 算法使用日志(Log)作為狀態變更的備忘錄;在云原生中,Kubernetes 的etcd
存儲集群狀態快照;在 AI 訓練中,模型檢查點(Checkpoint)是典型的備忘錄應用。未來趨勢是:備忘錄將與區塊鏈結合,每個區塊是系統狀態的不可變備忘錄;在量子計算中,量子態的測量與保存面臨“觀測即改變”的挑戰,需要新型備忘錄機制;在元宇宙中,用戶虛擬形象的狀態快照是跨世界遷移的基礎;在AI Agent 中,Agent 的記憶(Memory)可視為一種高級備忘錄,存儲其經驗與狀態。
掌握備忘錄模式,有助于設計出具備容錯性、可追溯性、用戶友好性的系統。作為架構師,應在設計需要“撤銷”、“回滾”或“歷史狀態管理”的功能時,優先考慮備忘錄模式。備忘錄不僅是模式,更是系統韌性的保障——它提醒我們:真正的健壯性,不僅在于正確執行操作,更在于當錯誤發生時,系統有能力優雅地“回到過去”,從錯誤中學習并恢復,而非陷入不可逆的崩潰。