備忘錄模式:實現(xiàn)撤銷/重做與狀態(tài)恢復的設計模式詳解
1. 項目概述為什么我們需要“后悔藥”在軟件開發(fā)的日常里我們經(jīng)常遇到一個場景用戶正在編輯一份復雜的文檔或者在一個圖形工具里繪制一幅精密的圖紙突然一個誤操作或者系統(tǒng)崩潰導致之前半小時的心血付諸東流。這時候用戶最渴望的就是一顆“后悔藥”能一鍵回到幾分鐘前的狀態(tài)。這種“保存快照、隨時恢復”的能力就是備忘錄模式要解決的核心問題。備忘錄模式英文叫Memento Pattern是23種經(jīng)典設計模式中行為型模式的一種。它的核心思想非常直觀就是在不破壞對象封裝性的前提下捕獲并外部化一個對象的內(nèi)部狀態(tài)以便在將來某個時刻可以將該對象恢復到這個狀態(tài)。簡單說就是給對象拍個“快照”然后把快照存起來等需要的時候再把這個快照“讀檔”回去。這個模式的名字起得非常貼切“備忘錄”就是用來記錄某個時刻的關(guān)鍵信息以備后查。這個模式的應用場景遠不止文檔編輯。在游戲開發(fā)中它是實現(xiàn)“存檔/讀檔”功能的基石在事務處理中它可以用來實現(xiàn)回滾操作在IDE里它是“撤銷/重做”功能背后的功臣。理解并掌握備忘錄模式意味著你為你的系統(tǒng)賦予了一種“狀態(tài)回溯”的超能力能極大地提升用戶體驗和系統(tǒng)的健壯性。接下來我們就深入拆解這個模式的里里外外看看如何從零開始把它穩(wěn)穩(wěn)地應用到你的項目里。2. 備忘錄模式的核心結(jié)構(gòu)與角色解析備忘錄模式的結(jié)構(gòu)清晰通常涉及三個核心角色它們各司其職共同協(xié)作完成狀態(tài)的保存與恢復。理解這三個角色及其關(guān)系是掌握這個模式的關(guān)鍵。2.1 發(fā)起人 (Originator)發(fā)起人是擁有需要被保存狀態(tài)的“當事人”。它知道當前時刻自身的所有內(nèi)部細節(jié)并且負責兩件關(guān)鍵事情創(chuàng)建備忘錄創(chuàng)建一個新的備忘錄對象并用自己當前的狀態(tài)來初始化這個備忘錄?;謴蜖顟B(tài)接收一個備忘錄對象并根據(jù)其中保存的信息將自己的狀態(tài)恢復到備忘錄所記錄的那個時刻。你可以把發(fā)起人想象成一個游戲角色它有自己的生命值、魔法值、裝備和位置。當需要存檔時它就負責把自己的這些屬性打包成一個“存檔文件”備忘錄。2.2 備忘錄 (Memento)備忘錄角色是狀態(tài)的“存儲箱”。它的唯一使命就是存儲發(fā)起人對象的內(nèi)部狀態(tài)。這里有一個非常重要的設計原則備忘錄對象通常只對發(fā)起人對象開放其狀態(tài)的讀寫權(quán)限而對其他對象比如管理者則隱藏其實現(xiàn)細節(jié)只提供一個窄接口。這完美體現(xiàn)了“封裝性”原則——狀態(tài)數(shù)據(jù)被安全地封裝在備忘錄內(nèi)部避免了被不相關(guān)的代碼隨意修改。備忘錄可以設計成兩種形式白箱實現(xiàn)備忘錄的所有字段都是public的任何對象都能訪問和修改。這種方式簡單但破壞了封裝性不推薦在生產(chǎn)中使用。黑箱實現(xiàn)推薦備忘錄將狀態(tài)存儲為私有字段并且其本身不提供任何public的修改方法。狀態(tài)的設置和獲取通過只有發(fā)起人能調(diào)用的包級私有或友元方法來完成在不同語言中實現(xiàn)方式不同如Java的包訪問權(quán)限、C的友元類。這是標準的、安全的設計。2.3 管理者 (Caretaker)管理者是備忘錄的“保管員”。它負責保存?zhèn)渫泴ο蟮^不應該也通常不能對備忘錄內(nèi)部存儲的狀態(tài)進行任何操作。管理者的職責很簡單當發(fā)起人說“幫我存?zhèn)€檔”它就接過備忘錄并收好當發(fā)起人說“把昨天的檔給我”它就把對應的備忘錄交還給發(fā)起人。管理者可以簡單地保存一個備忘錄也可以維護一個棧來實現(xiàn)多次撤銷Undo或者維護一個列表來實現(xiàn)多個存檔點。它是連接用戶操作如點擊“保存”按鈕和發(fā)起人狀態(tài)管理的橋梁。這三個角色的協(xié)作流程就像一個標準的存檔流程用戶觸發(fā)“保存”操作。管理者向發(fā)起人請求“請給我一個你當前的備忘錄快照”。發(fā)起人創(chuàng)建并返回一個包含自身當前狀態(tài)的備忘錄對象。管理者將這個備忘錄對象存儲起來放入棧、列表或文件。當用戶觸發(fā)“恢復”或“撤銷”操作時管理者將之前保存的備忘錄交還給發(fā)起人。發(fā)起人使用這個備忘錄將自己的狀態(tài)完全恢復到創(chuàng)建該備忘錄時的樣子。注意在設計備忘錄時務必考慮深度拷貝與淺拷貝的問題。如果發(fā)起人的狀態(tài)中包含對其他可變對象的引用如數(shù)組、集合、自定義對象簡單的字段拷貝淺拷貝會導致備忘錄和發(fā)起人共享同一份引用后續(xù)對狀態(tài)的修改會相互影響破壞快照的獨立性。因此在創(chuàng)建備忘錄時通常需要對復雜狀態(tài)進行深拷貝確??煺盏摹皟鼋Y(jié)”效果。3. 從理論到實踐一個文本編輯器的撤銷功能實現(xiàn)光說不練假把式我們用一個最經(jīng)典的例子——文本編輯器的撤銷Undo功能來完整走一遍備忘錄模式的實現(xiàn)。我們將使用Java語言因為它對面向?qū)ο筇匦缘闹С址浅G逦?。假設我們有一個簡單的文本編輯器它的核心是一個TextEditor類發(fā)起人可以輸入文本并且我們希望能撤銷到最后一次保存的狀態(tài)。3.1 第一步定義黑箱備忘錄首先我們實現(xiàn)一個“黑箱”備忘錄。關(guān)鍵點在于TextEditor可以訪問TextMemento的內(nèi)部狀態(tài)但外部的Caretaker不能。// 備忘錄類 - 對外部完全黑箱 public class TextMemento { // 私有字段存儲狀態(tài) private final String text; // 包級私有構(gòu)造器只有同包下的發(fā)起人能創(chuàng)建它 TextMemento(String textToSave) { this.text textToSave; } // 包級私有的獲取狀態(tài)方法只有同包下的發(fā)起人能讀取它 String getSavedText() { return this.text; } }注意TextMemento的構(gòu)造器和getSavedText()方法都是包級私有沒有public修飾符。這意味著只有與它位于同一個包例如com.example.memento下的類才能創(chuàng)建和讀取備忘錄。3.2 第二步實現(xiàn)發(fā)起人接下來實現(xiàn)文本編輯器本身即發(fā)起人角色。// 發(fā)起人類 - 文本編輯器 public class TextEditor { private StringBuilder text; // 使用StringBuilder便于文本操作 public TextEditor() { this.text new StringBuilder(); } // 業(yè)務方法添加文本 public void type(String words) { text.append(words); System.out.println(當前文本: text.toString()); } // 業(yè)務方法獲取當前文本 public String getText() { return text.toString(); } // **核心創(chuàng)建備忘錄** - 保存當前狀態(tài) public TextMemento save() { System.out.println(保存狀態(tài)...); // 創(chuàng)建備忘錄傳入當前文本的副本深拷貝思想這里String本身不可變所以沒問題 return new TextMemento(this.text.toString()); } // **核心恢復狀態(tài)** - 從備忘錄恢復 public void restore(TextMemento memento) { // 通過備忘錄的包級私有方法獲取保存的狀態(tài) this.text new StringBuilder(memento.getSavedText()); System.out.println(恢復狀態(tài)至: this.text.toString()); } }在save()方法中TextEditor創(chuàng)建了一個TextMemento對象。因為它們在同一個包內(nèi)所以可以調(diào)用TextMemento的包級私有構(gòu)造器。同樣在restore()方法中它可以調(diào)用getSavedText()方法。而對于包外的類這些細節(jié)都是不可見的。3.3 第三步實現(xiàn)管理者管理者Caretaker負責保管備忘錄。為了實現(xiàn)撤銷功能我們用一個棧Stack來保存歷史狀態(tài)。import java.util.Stack; // 管理者類 - 負責保管備忘錄歷史 public class Caretaker { // 使用棧來保存歷史狀態(tài)后進先出符合撤銷操作 private StackTextMemento history new Stack(); // 保存狀態(tài)到歷史記錄 public void saveState(TextMemento memento) { history.push(memento); } // 從歷史記錄中恢復最近一次狀態(tài)撤銷 public TextMemento undo() { if (!history.isEmpty()) { // 彈出最近一次保存的狀態(tài) return history.pop(); } return null; // 或者拋出一個異常表示無法撤銷 } // 可選查看歷史記錄深度 public int getHistorySize() { return history.size(); } }3.4 第四步客戶端代碼與運行演示最后我們編寫客戶端代碼將三者串聯(lián)起來模擬用戶的編輯和撤銷操作。// 客戶端代碼 public class Client { public static void main(String[] args) { // 1. 創(chuàng)建發(fā)起人編輯器和管理者歷史記錄器 TextEditor editor new TextEditor(); Caretaker history new Caretaker(); // 2. 用戶開始編輯 editor.type(Hello, ); // 3. 用戶覺得當前狀態(tài)不錯保存一下創(chuàng)建備忘錄并由管理者保存 history.saveState(editor.save()); editor.type(World!); System.out.println(繼續(xù)編輯后: editor.getText()); history.saveState(editor.save()); // 再次保存 editor.type( This is Memento Pattern.); System.out.println(再次編輯后: editor.getText()); // 4. 用戶想撤銷到最后一次保存的狀態(tài) System.out.println(\n--- 執(zhí)行撤銷操作 ---); TextMemento lastState history.undo(); if (lastState ! null) { editor.restore(lastState); System.out.println(撤銷后文本: editor.getText()); } // 5. 再撤銷一次 System.out.println(\n--- 再次執(zhí)行撤銷操作 ---); lastState history.undo(); if (lastState ! null) { editor.restore(lastState); System.out.println(再次撤銷后文本: editor.getText()); } System.out.println(剩余可撤銷次數(shù): history.getHistorySize()); } }運行結(jié)果預測當前文本: Hello, 保存狀態(tài)... 當前文本: Hello, World! 繼續(xù)編輯后: Hello, World! 保存狀態(tài)... 當前文本: Hello, World! This is Memento Pattern. 再次編輯后: Hello, World! This is Memento Pattern. --- 執(zhí)行撤銷操作 --- 恢復狀態(tài)至: Hello, World! 撤銷后文本: Hello, World! --- 再次執(zhí)行撤銷操作 --- 恢復狀態(tài)至: Hello, 再次撤銷后文本: Hello, 剩余可撤銷次數(shù): 0通過這個完整的例子你可以清晰地看到備忘錄模式如何優(yōu)雅地實現(xiàn)了狀態(tài)的保存與恢復。Caretaker完全不知道TextMemento里存了什么它只負責保管這個“黑盒子”這最大限度地保證了TextEditor內(nèi)部狀態(tài)的封裝性和安全性。4. 進階探討模式變體與實戰(zhàn)中的關(guān)鍵決策掌握了基礎實現(xiàn)后在實際項目中應用備忘錄模式你會面臨幾個關(guān)鍵的設計抉擇。不同的選擇會帶來不同的復雜度、性能和靈活性。4.1 增量備忘錄 vs. 全量備忘錄這是最核心的決策之一直接影響到性能和存儲開銷。全量備忘錄就像我們上面的例子每次保存都完整地拷貝發(fā)起人的整個狀態(tài)如整個文檔內(nèi)容。實現(xiàn)簡單恢復速度快直接替換但內(nèi)存消耗大。如果狀態(tài)很大如一張高清圖片、一個復雜模型頻繁保存歷史會導致內(nèi)存急劇增長。增量備忘錄只保存上一次狀態(tài)之后發(fā)生變化的部分差異。例如在文本編輯中只保存“在位置5插入了字符串‘ABC’”。這極大地節(jié)省了存儲空間特別適合狀態(tài)變化頻繁但增量小的場景。然而它的實現(xiàn)復雜得多恢復狀態(tài)時需要從某個基準狀態(tài)開始順序應用一系列增量變更恢復速度可能較慢且邏輯容易出錯。如何選擇狀態(tài)大小與變化頻率狀態(tài)大、變化部分小如文檔編輯優(yōu)先考慮增量。狀態(tài)本身不大或者變化總是全局性的用全量更省心?;謴托阅芤笠罂焖倩謴腿缬螒蜃x檔全量有優(yōu)勢??梢匀萑桃欢ㄓ嬎汩_銷的增量可行。復雜度權(quán)衡項目初期或原型階段用全量快速實現(xiàn)功能。后期性能成為瓶頸時再考慮重構(gòu)為增量。4.2 備忘錄的存儲與持久化備忘錄放在內(nèi)存里程序關(guān)閉就沒了。對于真正的“存檔”功能我們需要持久化到磁盤或數(shù)據(jù)庫。序列化這是最直接的方式。讓Memento類實現(xiàn)Serializable接口Java或類似機制。管理者保存時將對象序列化成字節(jié)流寫入文件恢復時從文件反序列化。優(yōu)點是簡單能保存復雜的對象圖。缺點是序列化格式通常與語言綁定不易跨語言讀取且類結(jié)構(gòu)變更如增加字段可能導致兼容性問題。自定義格式存儲將狀態(tài)轉(zhuǎn)換成一種自定義的、穩(wěn)定的數(shù)據(jù)格式如JSON、XML或Protocol Buffers。Originator在創(chuàng)建備忘錄時將狀態(tài)轉(zhuǎn)為JSON字符串恢復時再從JSON解析。優(yōu)點是格式人類可讀、跨語言、版本兼容性相對好處理。缺點是需要額外的轉(zhuǎn)換代碼對于非常復雜的嵌套對象轉(zhuǎn)換邏輯可能很繁瑣。命令式存儲這與增量備忘錄思想結(jié)合。不存儲狀態(tài)本身而是存儲導致狀態(tài)變化的命令序列如“AddTextCommand”, “DeleteCommand”?;謴蜁r重新執(zhí)行命令序列。這在圖形編輯器和某些游戲中很常見。實操心得對于需要長期保存、可能需跨版本兼容的存檔我強烈推薦JSON等自定義格式。雖然前期多寫一些轉(zhuǎn)換代碼但后期調(diào)試、數(shù)據(jù)遷移、甚至提供外部工具修改存檔都會方便得多。內(nèi)存中的備忘錄對象可以設計成包含一個toJson()和fromJson()方法。4.3 管理者的職責擴展歷史棧、分支與重做基礎的管理者只用一個棧實現(xiàn)撤銷。但完整的編輯器通常需要重做Redo功能這需要兩個棧——undoStack和redoStack。執(zhí)行撤銷時從undoStack彈出狀態(tài)壓入redoStack執(zhí)行重做時反之。新建操作會清空redoStack。歷史分支像Git一樣支持保存多個命名的快照點并可以在不同點之間切換。這需要管理者維護一個狀態(tài)節(jié)點圖每個節(jié)點是一個備忘錄節(jié)點間記錄父子關(guān)系。復雜度飆升但提供了強大的版本管理能力。狀態(tài)變更監(jiān)聽管理者可以監(jiān)聽發(fā)起人的狀態(tài)變更自動在合適的時機創(chuàng)建備忘錄如每隔5秒自動保存而不是完全由客戶端代碼驅(qū)動。5. 備忘錄模式的優(yōu)缺點與適用場景分析沒有一種設計模式是銀彈備忘錄模式也不例外。清晰認識其利弊才能做出正確的使用決策。5.1 優(yōu)勢封裝性得以保持這是它最大的優(yōu)點。通過黑箱實現(xiàn)發(fā)起人對象的內(nèi)部狀態(tài)細節(jié)被很好地隱藏在備忘錄內(nèi)部其他對象無法直接訪問和修改符合面向?qū)ο笤O計原則。簡化了發(fā)起人職責發(fā)起人無需自己管理歷史狀態(tài)只需負責創(chuàng)建和恢復備忘錄職責單一。狀態(tài)保存和管理的邏輯移交給了管理者。易于實現(xiàn)狀態(tài)回溯提供了一種標準化、可擴展的機制來實現(xiàn)撤銷、重做、事務回滾等需要回溯狀態(tài)的功能。管理者可以靈活管理歷史管理者可以決定保存?zhèn)渫浀念l率、存儲方式內(nèi)存、文件、數(shù)據(jù)庫和歷史結(jié)構(gòu)棧、列表、樹而不影響發(fā)起人的代碼。5.2 劣勢與挑戰(zhàn)資源消耗尤其是使用全量備忘錄時如果發(fā)起人對象狀態(tài)非常龐大如包含大圖片、復雜模型頻繁保存?zhèn)渫洉拇罅績?nèi)存和存儲空間。管理器職責過重在需要實現(xiàn)復雜歷史管理如分支、合并時管理者的邏輯會變得非常復雜。深拷貝的復雜性為了確保備忘錄狀態(tài)的獨立性往往需要深拷貝。如果對象圖非常深、包含循環(huán)引用深拷貝的實現(xiàn)會變得棘手且性能低下。潛在的語言限制在某些語言中實現(xiàn)真正的“黑箱”備忘錄僅對發(fā)起人可見可能需要使用一些特殊機制如友元類、內(nèi)部類、包訪問權(quán)限這可能不如其他模式通用。5.3 典型適用場景當你遇到以下情況時應該考慮備忘錄模式需要提供撤銷Undo和重做Redo功能文本編輯器、圖形繪圖軟件、IDE等。需要保存對象狀態(tài)快照并在之后恢復游戲中的存檔/讀檔功能。需要實現(xiàn)事務回滾數(shù)據(jù)庫操作或一系列業(yè)務操作在失敗時需要回滾到操作前的狀態(tài)。需要監(jiān)控對象狀態(tài)變化并能回溯到任意歷史點如配置管理、工作流狀態(tài)跟蹤。5.4 不適用或需謹慎使用的場景對象狀態(tài)極其龐大或復雜全量保存成本過高需評估是否能用增量模式或是否有其他更輕量的方案如命令模式記錄操作。狀態(tài)變更頻率極高例如實時游戲畫面每幀都保存狀態(tài)不現(xiàn)實。通常只在特定檢查點Checkpoint或用戶請求時保存。對性能有極端要求深拷貝和狀態(tài)恢復可能帶來性能開銷在性能關(guān)鍵路徑上需仔細評估。語言不支持必要的封裝特性如果無法實現(xiàn)安全的黑箱備忘錄導致狀態(tài)暴露則需要權(quán)衡其帶來的風險。6. 與其他相關(guān)模式的對比與抉擇備忘錄模式常與另外兩個行為型模式——命令模式和狀態(tài)模式——被放在一起討論和比較因為它們都與“狀態(tài)”和“行為”密切相關(guān)。理解它們的區(qū)別能幫助你在具體場景中做出更精準的選擇。6.1 備忘錄模式 vs. 命令模式這是最容易混淆的一對。兩者都能實現(xiàn)撤銷功能但思想截然不同。備忘錄模式關(guān)注于對象狀態(tài)的快照。它保存的是某一時刻對象的完整或增量數(shù)據(jù)。撤銷時直接用保存的數(shù)據(jù)覆蓋當前狀態(tài)。命令模式關(guān)注于將請求封裝為對象。它保存的是“做什么”的指令命令對象以及執(zhí)行該指令所需的參數(shù)。撤銷時命令對象會提供一個undo()方法該方法內(nèi)部通常封裝了反向操作邏輯例如AddTextCommand的undo()就是執(zhí)行刪除文本。如何選擇如果狀態(tài)本身易于保存和恢復且反向操作邏輯復雜或難以定義用備忘錄模式。例如保存一個復雜的游戲角色所有屬性。如果操作命令本身易于定義和反轉(zhuǎn)且狀態(tài)分散在多個對象中用命令模式。例如圖形編輯器中移動、旋轉(zhuǎn)、縮放圖形其反向操作很明確。在實踐中兩者常結(jié)合使用命令對象在執(zhí)行時可以先創(chuàng)建受影響對象的備忘錄并保存起來。當需要撤銷該命令時命令的undo()方法就利用這個備忘錄來恢復對象狀態(tài)。這樣既利用了命令模式組織操作的靈活性又利用了備忘錄模式恢復狀態(tài)的直接性。6.2 備忘錄模式 vs. 原型模式原型模式用于克隆對象它也能得到一個對象在某一時刻的副本。區(qū)別在于備忘錄模式目的是狀態(tài)恢復關(guān)注點在于將對象回滾到某個歷史狀態(tài)。備忘錄對象可能只保存部分狀態(tài)且其生命周期由管理者控制。原型模式目的是創(chuàng)建新對象關(guān)注點在于以現(xiàn)有對象為藍本高效地生成一個獨立的新實例??寺〉玫降氖且粋€完整的、可獨立使用的新對象。6.3 備忘錄模式 vs. 狀態(tài)模式狀態(tài)模式允許一個對象在其內(nèi)部狀態(tài)改變時改變其行為。它和備忘錄模式的聯(lián)系在于一個狀態(tài)對象本身可能就包含了需要保存的數(shù)據(jù)。你可以為狀態(tài)模式中的每個具體狀態(tài)類實現(xiàn)創(chuàng)建備忘錄和恢復狀態(tài)的方法。當發(fā)起人上下文保存狀態(tài)時實際上可能需要遍歷并保存其內(nèi)部所有狀態(tài)對象的信息。備忘錄模式可以作為狀態(tài)模式的一個輔助機制用于保存和恢復整個狀態(tài)機的配置。7. 實戰(zhàn)避坑指南與性能優(yōu)化技巧紙上得來終覺淺絕知此事要躬行。在實際項目中使用備忘錄模式我踩過不少坑也總結(jié)出一些讓模式用得更“順滑”的技巧。7.1 深拷貝的陷阱與解決方案這是備忘錄模式最大的坑之一。淺拷貝會導致備忘錄和原始對象共享引用一改俱改。問題示例public class Originator { private ListString items new ArrayList(); // 可變對象引用 public Memento save() { // 錯誤這只是拷貝了引用items列表在備忘錄和Originator中是同一個對象 return new Memento(this.items); } }解決方案使用不可變對象盡可能將狀態(tài)設計為不可變對象如String、Integer。這樣淺拷貝也是安全的。手動實現(xiàn)深拷貝對于集合、數(shù)組或自定義對象在創(chuàng)建備忘錄時手動創(chuàng)建它們的副本。public Memento save() { // 正確創(chuàng)建列表的深拷貝 ListString copyOfItems new ArrayList(this.items); return new Memento(copyOfItems); }序列化/反序列化利用Java的序列化機制進行深拷貝要求所有相關(guān)類實現(xiàn)Serializable。這種方法能處理復雜的對象圖但性能開銷較大。public Memento save() throws IOException, ClassNotFoundException { ByteArrayOutputStream bos new ByteArrayOutputStream(); ObjectOutputStream oos new ObjectOutputStream(bos); oos.writeObject(this.items); oos.close(); return new Memento(bos.toByteArray()); } public void restore(Memento m) throws IOException, ClassNotFoundException { ByteArrayInputStream bis new ByteArrayInputStream(m.getState()); ObjectInputStream ois new ObjectInputStream(bis); this.items (ListString) ois.readObject(); ois.close(); }使用第三方庫如Apache Commons Lang的SerializationUtils.clone()或使用JSON序列化/反序列化來實現(xiàn)深拷貝。7.2 內(nèi)存管理與歷史記錄清理如果無限制地保存?zhèn)渫泝?nèi)存遲早會耗盡。設置歷史深度上限在Caretaker中只保留最近N條記錄。當超過上限時丟棄最舊的記錄。這對于文本編輯器的撤銷功能是常見做法。private StackMemento undoStack new Stack(); private static final int MAX_HISTORY 50; public void saveState(Memento m) { undoStack.push(m); if (undoStack.size() MAX_HISTORY) { // 移除棧底最舊的元素需要一些額外邏輯因為Stack不直接支持 // 更簡單的做法是使用DequeArrayDeque } }定期清理或按需保存不要每次狀態(tài)微小的變化都保存。可以設置一個閾值如文本變化超過10個字符或者由用戶顯式觸發(fā)保存如CtrlS。增量存儲結(jié)合壓縮對于增量備忘錄可以定期將多個增量合并成一個全量快照并清除中間的增量記錄以節(jié)省空間和加速恢復。7.3 處理復雜對象圖的快照當發(fā)起人的狀態(tài)是一個包含大量相互引用對象的復雜圖時創(chuàng)建快照會非常困難。標識對象引用在備忘錄中不存儲對象本身而是存儲對象的唯一標識符ID?;謴蜁r根據(jù)ID從一個全局的“對象倉庫”中獲取對象。這要求所有對象在倉庫中是可檢索的并且其狀態(tài)在保存后不會被意外修改。使用“寫時復制”技術(shù)如果狀態(tài)對象本身支持不可變視圖或拷貝可以延遲拷貝的時機。在創(chuàng)建備忘錄時只標記需要保存直到真正恢復時如果發(fā)現(xiàn)原始對象已被修改再執(zhí)行實際的拷貝操作。這需要更精細的控制。領域驅(qū)動設計中的聚合根在DDD中通常只對聚合根進行備忘錄保存。聚合內(nèi)部的其他對象狀態(tài)通過根來保持一致。這簡化了快照的邊界。7.4 線程安全考量如果發(fā)起人對象可能在多線程環(huán)境下被修改那么創(chuàng)建備忘錄和恢復狀態(tài)的操作必須是原子的否則可能保存到不一致的中間狀態(tài)。同步Synchronized在save()和restore()方法上使用synchronized關(guān)鍵字確保同一時間只有一個線程能執(zhí)行狀態(tài)保存或恢復。使用不可變快照設計備忘錄對象為完全不可變的。這樣即使在保存過程中發(fā)起人狀態(tài)發(fā)生變化備忘錄持有的也是創(chuàng)建那一刻的確定狀態(tài)。這通常需要在save()方法內(nèi)部將所需狀態(tài)先拷貝到局部變量再用這些局部變量構(gòu)造備忘錄。版本號或時間戳為每個備忘錄增加一個版本號或創(chuàng)建時間戳。管理者在恢復時可以檢查版本是否匹配或者讓用戶選擇恢復到哪個時間點的狀態(tài)。備忘錄模式是一個強大而優(yōu)雅的工具它將狀態(tài)管理的復雜性從業(yè)務對象中剝離出來賦予了系統(tǒng)“時光倒流”的能力。從簡單的文本撤銷到復雜的游戲存檔其思想一脈相承。關(guān)鍵在于理解其封裝狀態(tài)的核心思想并根據(jù)你的具體場景在全量與增量、內(nèi)存與持久化、簡單與復雜之間做出恰當?shù)臋?quán)衡。下次當你需要給用戶一顆“后悔藥”時不妨想想備忘錄模式它很可能就是那個優(yōu)雅的解決方案。

相關(guān)新聞

【愚公系列】《WorkBuddy從上手到變現(xiàn)》014-用AI Agent實現(xiàn)公眾號自動化運營(案例:1人運營13個平臺)

【愚公系列】《WorkBuddy從上手到變現(xiàn)》014-用AI Agent實現(xiàn)公眾號自動化運營(案例:1人運營13個平臺)

💎【行業(yè)認證權(quán)威頭銜】 ? 華為云天團核心成員:特約編輯/云享專家/開發(fā)者專家/產(chǎn)品云測專家 ? 開發(fā)者社區(qū)全滿貫:CSDN博客&商業(yè)化雙料專家/阿里云簽約作者/騰訊云內(nèi)容共創(chuàng)官/掘金&亞馬遜&51CTO頂級博主 ? 技術(shù)生態(tài)共建先鋒&am…

2026/8/3 8:48:39 閱讀更多
5個簡單技巧:用Seraphine英雄聯(lián)盟助手提升你的排位勝率

5個簡單技巧:用Seraphine英雄聯(lián)盟助手提升你的排位勝率

5個簡單技巧:用Seraphine英雄聯(lián)盟助手提升你的排位勝率 【免費下載鏈接】Seraphine 英雄聯(lián)盟戰(zhàn)績查詢工具 項目地址: https://gitcode.com/gh_mirrors/se/Seraphine 還在為英雄聯(lián)盟排位賽中的BP決策煩惱嗎?Seraphine是一款基于官方LCU API開發(fā)的英…

2026/8/3 10:48:45 閱讀更多
公章丟了怎么登報掛失?需要多少錢?2026登報渠道對比

公章丟了怎么登報掛失?需要多少錢?2026登報渠道對比

截至2026年8月,公章丟失后可通過線上小程序或報社柜臺登報。辦理重點是選對報紙,寫對企業(yè)名稱、公章類型和編號。可使用微信或支付寶里面的慧辦好登報小程序,按城市查詢?nèi)珖l(fā)行、省級、地市級報紙,并確認價格、見報日期和原報寄送…

2026/8/3 10:48:45 閱讀更多
VR技術(shù)在跨步電壓安全培訓中的應用與實踐

VR技術(shù)在跨步電壓安全培訓中的應用與實踐

1. 項目背景與核心價值 去年夏天在檢修變電站時,我親眼目睹一名新手電工因為誤判跨步電壓范圍差點釀成事故。這種觸電風險在電力行業(yè)其實非常普遍——根據(jù)行業(yè)安全報告,近三年由跨步電壓引發(fā)的觸電事故占比高達17%,而傳統(tǒng)安全教育方式存在兩大…

2026/8/3 10:38:44 閱讀更多
全球僅7家廠商通過ISO/IEC 27001認證的名片AI引擎,我們逆向拆解了它的字段置信度熔斷機制

全球僅7家廠商通過ISO/IEC 27001認證的名片AI引擎,我們逆向拆解了它的字段置信度熔斷機制

更多請點擊: https://kaifayun.com 第一章:全球僅7家廠商通過ISO/IEC 27001認證的名片AI引擎概覽 名片AI引擎是企業(yè)級智能文檔處理的核心組件,專注于高精度OCR、語義結(jié)構(gòu)化提取與跨語言實體對齊。截至2024年第三季度,全球范圍內(nèi)僅…

2026/8/3 0:07:47 閱讀更多
3分鐘搞定!QQ空間歷史說說完整備份終極指南

3分鐘搞定!QQ空間歷史說說完整備份終極指南

3分鐘搞定!QQ空間歷史說說完整備份終極指南 【免費下載鏈接】GetQzonehistory 獲取QQ空間發(fā)布的歷史說說 項目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否曾想過,那些年發(fā)過的QQ空間說說,那些記錄青春的文字…

2026/8/2 0:04:01 閱讀更多
AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O分配PCB板是應用材料(Applied Materials)公司生產(chǎn)的一款用于半導體設備的I/O信號分配電路板。該型號(0100-02186)的核心特點如下:專用于Endura等半導體工藝腔室。集成信號路由與分配功能。連接控制…

2026/8/2 2:51:21 閱讀更多
Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動機

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動機

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動機是日本日清(Nissei)品牌的一款工業(yè)用三相異步電機,適用于自動化設備及通用機械驅(qū)動。該型號(FFMN-32L-10-T0 40AX)的核心特點如下:三相交流異步電動機。額定…

2026/8/2 2:52:49 閱讀更多