設計模式(十九)行為型:備忘錄模式詳解

設計模式(十九)行為型:備忘錄模式詳解

備忘錄模式(Memento Pattern)是 GoF 23 種設計模式中的行為型模式之一,其核心價值在于在不破壞封裝性的前提下,捕獲并外部化一個對象的內部狀態,以便在之后能夠將該對象恢復到原先保存的狀態。它通過引入“備忘錄”對象來存儲原發器(Originator)的快照,由管理者(Caretaker)負責保存和管理這些快照,從而實現狀態的保存與恢復。備忘錄模式是實現撤銷(Undo)、重做(Redo)、事務回滾、游戲存檔、對象版本控制、配置快照等關鍵功能的基礎,是構建具備“時間旅行”能力、高容錯性和用戶友好性的系統的必備設計模式。

一、詳細介紹

備忘錄模式解決的是“需要保存對象在某一時刻的狀態,并在需要時恢復該狀態,但又不能暴露對象的內部實現細節”的問題。在傳統設計中,若要實現撤銷功能,可能會讓對象提供 getState()setState() 方法,但這會破壞對象的封裝性,暴露其內部結構,導致耦合度高、安全性差。

備忘錄模式的核心思想是:將狀態的保存與恢復過程封裝在三個角色中,通過“窄接口”與“寬接口”的設計,既保證了封裝性,又實現了狀態管理

該模式包含以下核心角色:

  • Originator(原發器):創建一個備忘錄來記錄當前內部狀態,并可在需要時根據備忘錄恢復狀態。它負責定義如何保存和恢復自身狀態。
  • Memento(備忘錄):存儲原發器的內部狀態。備忘錄對象的接口設計是關鍵:
    • 窄接口(Narrow Interface):對外(管理者)只暴露最小必要接口,通常無任何方法(或僅用于標識),防止管理者修改狀態。
    • 寬接口(Wide Interface):對原發器暴露完整狀態訪問權限,允許原發器讀取狀態以恢復自身。
      在 Java 中,通常通過內部類包級私有類實現,利用訪問控制機制實現接口隔離。
  • Caretaker(管理者):負責保存和管理備忘錄對象,但不能對備忘錄的內容進行任何操作或檢查。它只是“保管”備忘錄,并在需要時將其交還給原發器。

備忘錄模式的關鍵優勢:

  • 保持封裝性:原發器的內部狀態通過備忘錄安全地外部化,管理者無法訪問或修改。
  • 支持撤銷/重做:通過保存多個備忘錄(如棧結構),可實現多級撤銷與重做。
  • 實現事務回滾:在操作失敗時,可恢復到操作前的狀態。
  • 支持版本控制:可保存對象的歷史狀態快照。
  • 解耦狀態管理:狀態的保存與恢復邏輯由原發器控制,管理者僅負責存儲。

與“命令模式”相比,命令封裝操作,備忘錄封裝狀態;命令常與備忘錄結合實現撤銷功能(命令執行前保存狀態)。與“觀察者模式”相比,觀察者關注狀態變化的通知,備忘錄關注狀態的保存與恢復

備忘錄模式適用于:

  • 需要實現撤銷/重做功能(如文本編輯器、圖形編輯器)。
  • 需要事務性操作或回滾機制。
  • 需要保存游戲進度或配置快照。
  • 需要對象狀態的歷史版本管理。

二、備忘錄模式的UML表示

以下是備忘錄模式的標準 UML 類圖:

creates and uses
stores
Originator
-state: String
+setState(state: String)
+getState()
+createMemento()
+restore(memento: Memento)
Memento
-state: String
+getState()
Caretaker
-mementos: List<Memento>
+addMemento(memento: Memento)
+getMemento(index: int)

圖解說明

  • Originator 創建 Memento 保存狀態,并可從 Memento 恢復狀態。
  • Memento 存儲 Originator 的狀態。
  • Caretaker 保存 Memento 對象,但無法訪問其內容。
  • 實際實現中,MementogetState() 方法通常設為 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圖(簡化版)
creates and restores
stores
TextEditor
-content: String
+type(text: String)
+getContent()
+save()
+restore(memento: EditorMemento)
EditorMemento
-content: String
-timestamp: long
+getContent()
+getTimestamp()
History
-mementos: List<EditorMemento>
+push(memento: EditorMemento)
+pop()
+isEmpty()

運行說明

  • TextEditor 是原發器,維護文本內容。
  • EditorMemento 是備忘錄,存儲文本快照和時間戳。
  • History 是管理者,使用棧結構保存備忘錄,實現撤銷功能。
  • 客戶端通過 save() 創建備忘錄并交由 History 保存。
  • 通過 pop() 獲取備忘錄并調用 restore() 恢復狀態。
  • EditorMementogetContent() 為包私有,僅 TextEditor 可訪問,History 無法讀取內容,保證封裝性。

四、總結

特性說明
核心目的安全保存并恢復對象狀態,保持封裝性
實現機制原發器創建備忘錄,管理者存儲,原發器恢復
優點保持封裝性、支持撤銷/重做、實現回滾、解耦狀態管理
缺點可能消耗大量內存(保存多個快照)、管理復雜狀態時備忘錄設計復雜
適用場景撤銷/重做、事務回滾、游戲存檔、配置快照、版本控制
不適用場景狀態極小或無需保存、性能極度敏感、狀態頻繁變化

備忘錄模式使用建議

  • 使用窄接口設計,防止管理者訪問內部狀態。
  • 考慮內存開銷,可實現快照壓縮、增量保存或限制歷史記錄長度。
  • 可結合命令模式,命令對象在執行前保存備忘錄。
  • 在 Java 中,可使用內部類實現備忘錄,天然支持訪問控制。

架構師洞見:
備忘錄模式是“狀態持久化”與“時間控制”的抽象。在現代架構中,其思想已演變為事件溯源(Event Sourcing)CQRS(命令查詢職責分離)數據庫事務日志分布式快照 的核心。例如,在事件溯源中,對象狀態由事件流重建,每個事件相當于一個“操作備忘錄”;在分布式系統中,Raft 或 Paxos 算法使用日志(Log)作為狀態變更的備忘錄;在云原生中,Kubernetes 的 etcd 存儲集群狀態快照;在 AI 訓練中,模型檢查點(Checkpoint)是典型的備忘錄應用。

未來趨勢是:備忘錄將與區塊鏈結合,每個區塊是系統狀態的不可變備忘錄;在量子計算中,量子態的測量與保存面臨“觀測即改變”的挑戰,需要新型備忘錄機制;在元宇宙中,用戶虛擬形象的狀態快照是跨世界遷移的基礎;在AI Agent 中,Agent 的記憶(Memory)可視為一種高級備忘錄,存儲其經驗與狀態。

掌握備忘錄模式,有助于設計出具備容錯性、可追溯性、用戶友好性的系統。作為架構師,應在設計需要“撤銷”、“回滾”或“歷史狀態管理”的功能時,優先考慮備忘錄模式。備忘錄不僅是模式,更是系統韌性的保障——它提醒我們:真正的健壯性,不僅在于正確執行操作,更在于當錯誤發生時,系統有能力優雅地“回到過去”,從錯誤中學習并恢復,而非陷入不可逆的崩潰。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/91420.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/91420.shtml
英文地址,請注明出處:http://en.pswp.cn/web/91420.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Qt/C++開發監控GB28181系統/錄像回放/切換播放進度立即跳轉/支持8倍速播放/倍速和跳轉進度無縫切換

一、前言說明 在國標監控系統中&#xff0c;錄像回放過程中&#xff0c;需要切換播放進度&#xff0c;對比過很過國標系統&#xff0c;絕大部分尤其是網頁版的監控系統&#xff0c;在切換進度過程中都會黑屏&#xff0c;這個體驗就很不友好了&#xff0c;明明gb28181協議中就有…

【11】大恒相機SDK C++開發 ——原圖像數據IFrameData內存中上下顛倒,怎么裁剪ROI 實時顯示在pictureBox中

文章目錄3 當內存中的 圖像數據是垂直翻轉的時候怎么截取ROI 并顯示3.1 對ROI在原圖中的位置做轉換3.2 將ROI的最后一行當做開始位置&#xff0c;從底部向上復制數據3.3 完整代碼3.4 圖像數據在內存中上下顛倒的情況3.5 調用驗證4 unsafe代碼 解釋及注意事項 看我另一篇文章5 C…

小架構step系列29:校驗注解的組合

1 概述如果遇到某些屬性需要多種校驗&#xff0c;比如需要非空、符合某正則表達式、長度不能超過某值等&#xff0c;如果這種屬性只有有限幾個&#xff0c;那么手工把對應的校驗注解都加上即可。但如果這種屬性比較多&#xff0c;那么重復加這些校驗注解&#xff0c;也是一種代…

網絡基礎19:OSPF多區域實驗

一、拓撲結構1. 網絡拓撲&#xff1a;骨干區域&#xff08;Area 0&#xff09;&#xff1a;連接核心設備&#xff08;AR1、AR2、AR3、AR4、AR5、AR6&#xff09;。非骨干區域&#xff1a;Area 1&#xff1a;AR5 ? AR9Area 2&#xff1a;AR5 ? AR10Area 3&#xff1a;AR6 ? A…

goland編寫go語言導入自定義包出現: package xxx is not in GOROOT (/xxx/xxx) 的解決方案

問題 寫了個自定義的包 calc.go&#xff0c;在路徑 $GOPATH/go_project/src/demo_51_package/com/目錄下&#xff0c;其中main.go 是main方法的入口代碼 main.go 代碼如下 package main import "demo_51_package/com" func main() {add : calc.Add(1, 2)println(add)…

HLS視頻切片音頻中斷問題分析與解決方案

HLS視頻切片音頻中斷問題分析與解決方案 問題背景 在使用FFmpeg進行HLS視頻切片并通過hls.js前端播放時&#xff0c;開發者經常遇到一個典型問題&#xff1a;第一個視頻切片播放正常且有聲音&#xff0c;但后續切片卻突然失去音頻。這種現象在直播和點播場景中均有出現&#xf…

【Linux網絡編程】網絡層協議 - IP

目錄 背景補充 協議頭格式 IP報文的分片與組裝 網段劃分 網段劃分是什么&#xff1f;為什么要進行網段劃分&#xff1f; 怎么進行網段劃分&#xff1f; 路由 路由表生成算法 背景補充 假設現在主機B要給主機C發送消息。在我們前面的學習中&#xff0c;一直都是將數據拷…

從“救火”到“先知”:潤建曲尺運維大模型如何重構網絡運維價值鏈

“7月18號&#xff0c;北京&#xff0c;晴&#xff0c;最高溫度38攝氏度。”天氣預報緩緩播報&#xff0c;商場、地鐵、辦公樓無不歌頌著威利斯開利的貢獻&#xff0c;但這份涼爽的背后&#xff0c;離不開 “電” 的無聲托舉。5G毫秒級下載、絲滑的移動支付、智能電表、智能家居…

Element表格單元格類名動態設置

在 Element UI 的 el-table 組件中&#xff0c;cell-class-name 屬性用于動態自定義表格單元格的 CSS 類名&#xff0c;通常用于根據數據條件設置樣式。1. 基本用法在 el-table 上綁定 :cell-class-name 屬性&#xff0c;值為一個函數。該函數接收一個對象參數&#xff0c;返回…

利用容器適配器實現stack和queue外加deque的介紹(STL)

文章目錄前言什么是容器適配器&#xff1f;觀察庫中的源碼那么該如何使用容器適配器呢&#xff1f;deque的簡單介紹(了解)deque的原理介紹deque的優缺為什么選擇deque作為stack和queue的底層默認容器&#xff1f;&#xff08;重點&#xff09;利用容器適配器實現我們自己的棧和…

【因子動物園巡禮】第12章:機器學習在因子投資中的應用(中文翻譯)

【因子動物園巡禮】第12章&#xff1a;機器學習在因子投資中的應用&#xff08;中文翻譯&#xff09;第12章 因子投資中的機器學習12.1 量化金融中的人工智能12.2 量化因子投資的AI化組件&#xff1a;解剖學視角12.2.1 數據源拓展與預處理12.2.2 因子研究12.2.3 因子模型12.2.4…

【Golang】用官方rate包構造簡單IP限流器

文章目錄使用 Go 實現基于 IP 地址的限流機制什么是 IP 限流&#xff1f;基于 rate.Limiter 實現 IP 限流1. 設計思路2. 代碼實現3. 限流中間件4. 在 Gin 中使用中間件代碼解釋使用 Go 實現基于 IP 地址的限流機制 在高流量的服務中&#xff0c;限流是一個至關重要的環節。它不…

力扣 Pandas 挑戰(6)---數據合并

本文圍繞力扣的Pandas簡單題集&#xff0c;解析如何用Pandas完成基礎數據處理任務&#xff0c;適合Pandas初學者學習。題目1&#xff1a;1050. 合作過至少三次的演員和導演題目描述&#xff1a;ActorDirector 表&#xff1a;---------------------- | Column Name | Type | …

隨筆之TDengine基準測試示例

文章目錄一、基本信息二、基準測試策略三、基準測試過程1. 模擬高并發寫入場景2. 模擬并發查詢場景四、基準測試結論一、基本信息 TDengine 版本&#xff1a;3.3.6.13&#xff08;目前最新版本&#xff09;服務器配置&#xff1a;16核CPU&#xff0c;32GB內存&#xff0c;高IO…

【IQA技術專題】DISTS代碼講解

本文是對DISTS圖像質量評價指標的代碼解讀&#xff0c;原文解讀請看DISTS文章講解。 本文的代碼來源于IQA-Pytorch工程。 1、原文概要 以前的一些IQA方法對于捕捉紋理上的感知一致性有所欠缺&#xff0c;魯棒性不足。基于此&#xff0c;作者開發了一個能夠在圖像結構和圖像紋…

2024年SEVC SCI2區,一致性虛擬領航者跟蹤群集算法GDRRT*-PSO+多無人機路徑規劃,深度解析+性能實測

目錄1.摘要2.算法背景3.GDRRT*-PSO與虛擬領航者跟蹤算法4.結果展示5.參考文獻6.算法輔導應用定制讀者交流1.摘要 隨著無人機技術的快速發展及其卓越的運動和機動性能&#xff0c;無人機在社會和軍事等諸多領域得到了廣泛應用。多無人機協同作業&#xff0c;能夠顯著提升任務執…

鏈特異性文庫是什么?為什么它在轉錄組測序中越來越重要?

鏈特異性文庫是什么&#xff1f;為什么它在轉錄組測序中越來越重要&#xff1f; 在現代分子生物學研究中&#xff0c;RNA測序&#xff08;RNA-seq&#xff09; 是一種廣泛應用的技術&#xff0c;用于分析基因在不同條件下的表達情況。而在RNA-seq的眾多技術細節中&#xff0c;有…

ClickHouse vs PostgreSQL:數據分析領域的王者之爭,誰更勝一籌?

文章概要 作為一名數據架構師&#xff0c;我經常被問到一個問題&#xff1a;在眾多數據庫選擇中&#xff0c;ClickHouse和PostgreSQL哪一個更適合我的項目&#xff1f;本文將深入探討這兩種數據庫系統的核心差異、性能對比、適用場景以及各自的優缺點&#xff0c;幫助您在技術選…

面向對象系統的單元測試層次

面向對象系統的單元測試層次面向對象&#xff08;Object-Oriented, OO&#xff09;編程范式引入了封裝、繼承和多態等核心概念&#xff0c;這使得傳統的、基于函數的單元測試方法不再充分。面向對象系統的單元測試必須適應其獨特的結構和行為特性&#xff0c;從單一方法擴展到類…

如何用USRP捕獲手機信號波形(上)系統及知識準備

目錄&#xff1a; 如何用USRP捕獲手機信號波形&#xff08;上&#xff09;系統及知識準備 如何用USRP捕獲手機信號波形&#xff08;中&#xff09;手機/基站通信 如何用USRP捕獲手機信號波形&#xff08;下&#xff09;協議分析 一、手機通信參數獲取 首先用Cellular-z網絡…