談 C++17 裏的 Memento 模式

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"備忘錄模式:介紹相關概念並實現一個較全面的 Undo Manager 類庫。","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Memento Pattern","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"動機","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"備忘錄模式也是一種行爲設計模式。它在 Ctrl-Z 或者說 Undo/Redo 場所中時最爲重要,這裏也是它的最佳應用場所。除此之外,有時候我們也可以稱之爲存檔模式,你可以將其泛化到一切備份、存檔、快照的場景裏,例如 macOS 的 Time Machine。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Memento 之所以能成爲一種 Pattern,就在於它已經將上述場景進行了抽象和掩蓋。在這裏討論備忘錄模式時一定需要注意到它作爲一種設計模式所提供的最強大的能力:不是能夠 Undo/Redo,而是能夠掩蓋細節。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然要以文字編輯器的 Undo/Redo 場景爲例來說明這一點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Memento 模式會掩蓋編輯器編輯命令的實現細節,例如編輯位置、鍵擊事件、修改的文字內容等等,僅僅只是將它們打包爲一條編輯記錄總體地提供給外部。外部使用者無需瞭解所謂的實現細節,它只需要發出 Undo 指令,就能從編輯歷史中抽出並回退一條編輯記錄,從而完成 Undo 動作。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這就是理想中的 Memento 模式應該要達到的效果。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/aa/aa93f835bce2f229a6dddc4b11d40b09.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"輕量的古典定義","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面提到的字處理器設計是較爲豐滿的案例。實際上多數古典的如 GoF 的 Memento 模式的定義是比較輕量級的,它們通常涉及到三個對象:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"originator :","attrs":{}},{"type":"text","text":" 創始人通常是指擁有狀態快照的對象,狀態快照由創始人負責進行創建以便於將來從備忘錄中恢復。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"memento :","attrs":{}},{"type":"text","text":" 備忘錄儲存狀態快照,一般來說這是個 POJO 對象。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"caretaker :","attrs":{}},{"type":"text","text":" 負責人對象負責追蹤多個 memento 對象。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它的關係圖是這樣的:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a5/a5cc0bf992c9c50356adfeba3d4f365c.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FROM: ","attrs":{}},{"type":"link","attrs":{"href":"https://upload.wikimedia.org/wikipedia/commons/3/38/W3sDesign_Memento_Design_Pattern_UML.jpg","title":"","type":null},"content":[{"type":"text","text":"Here","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個略有調整的 C++ 實現是這樣的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"namespace dp { namespace undo { namespace basic {\n\n template\n class memento_t {\n public:\n ~memento_t() = default;\n\n void push(State &&s) {\n _saved_states.emplace_back(s);\n dbg_print(\" . save memento state : %s\", undo_cxx::to_string(s).c_str());\n }\n std::optional pop() {\n std::optional ret;\n if (_saved_states.empty()) {\n return ret;\n }\n ret.emplace(_saved_states.back());\n _saved_states.pop_back();\n dbg_print(\" . restore memento state : %s\", undo_cxx::to_string(*ret).c_str());\n return ret;\n }\n auto size() const { return _saved_states.size(); }\n bool empty() const { return _saved_states.empty(); }\n bool can_pop() const { return !empty(); }\n\n private:\n std::list _saved_states;\n };\n\n template>\n class originator_t {\n public:\n originator_t() = default;\n ~originator_t() = default;\n\n void set(State &&s) {\n _state = std::move(s);\n dbg_print(\"originator_t: set state (%s)\", undo_cxx::to_string(_state).c_str());\n }\n void save_to_memento() {\n dbg_print(\"originator_t: save state (%s) to memento\", undo_cxx::to_string(_state).c_str());\n _history.push(std::move(_state));\n }\n void restore_from_memento() {\n _state = *_history.pop();\n dbg_print(\"originator_t: restore state (%s) from memento\", undo_cxx::to_string(_state).c_str());\n }\n\n private:\n State _state;\n Memento _history;\n };\n\n template\n class caretaker {\n public:\n caretaker() = default;\n ~caretaker() = default;\n void run() {\n originator_t o;\n o.set(\"state1\");\n o.set(\"state2\");\n o.save_to_memento();\n o.set(\"state3\");\n o.save_to_memento();\n o.set(\"state4\");\n\n o.restore_from_memento();\n }\n };\n\n}}} // namespace dp::undo::basic\n\nvoid test_undo_basic() {\n using namespace dp::undo::basic;\n caretaker<:string> c;\n c.run();\n}\n\nint main() {\n test_undo_basic();\n return 0;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個實現代碼中對於負責人部分的職責進行了簡化,將相當多的任務交給其他人去完成,目的是在於讓使用者的編碼能夠更簡單。使用者只需要像 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"caretaker","attrs":{}}],"attrs":{}},{"type":"text","text":" 那樣去操作 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"originator_t","attrs":{}}],"attrs":{}},{"type":"text","text":" 就能夠完成 memento 模式的運用。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"應用場景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單的場景可以直接複用上面的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"memento_t","attrs":{}}],"attrs":{}},{"type":"text","text":" 和 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"originator_t","attrs":{}}],"attrs":{}},{"type":"text","text":" 模板,它們雖然簡易,但足以應付一般場景了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"抽象地看待 memento 模式,於一開始我們就提到了一切備份、存檔、快照的場景裏都可以應用 Memento 模式,所以總是不免會用複雜或者說通用的場景需求。這些複雜的需求就不是 memento_t 所能應付的了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了那些備份存檔快照場景之外,有時候有的場景或許你併爲意識到它們也可以以 memory history 的方式來看待。例如博客日誌的時間線展示,實際上就是一個 memento list。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實際上,爲了給你加深印象,在以類庫開發爲已任的生活中我們一般會用 undo_manager 這種東西來表達和實現通用型的 Memento 模式。所以順理成章地,下面我們將嘗試用 Memento 的理論來指導實作一個 undoable 通用型模板類。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Memento 模式通常並不能獨立存在,它多半是作爲 Command Pattern 的一個子系統(又或是並立而協作的模塊)而存在的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以典型的編輯器架構設計裏,總是將文字操作設計爲 Command 模式,例如前進一個 word,鍵入幾個字符,移動插入點到某個位置,粘貼一段剪貼板內容,等等,都是由一系列的 Commands 來具體實施的。在此基礎上,Memento 模式纔有工作空間,它將能夠利用 EditCommand 來實現編輯狀態的存儲和重播——通過反向播放與重播一條(組)Edit Commands 的方式。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Undo Manager","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"UndoRedo 模型原本是一種交互性技術,爲用戶提供反悔的能力,後來逐漸演變爲一種計算模型。通常 Undo 模型被區分爲兩種類型:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"線性的","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"非線性的","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"線性 Undo 是以 Stack 的方式實現的,一條新的命令被執行後就會被添加到棧頂。也因此 Undo 系統能夠反序回退已經執行過的命令,且只能是依次反序回退的,所以這種方式實現的 Undo 系統被稱作爲線性的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在線性 Undo 系統中還有嚴格線性模式一說,通常這種提法是指 undo 歷史是有尺寸上限的,正如 CPU 體系中的 Stack 也是有尺寸限制的一個道理。在很多矢量繪圖軟件中,Undo 歷史被要求設定爲一個初值,例如 1 到 99 之間,如果回退歷史表過大,則最陳舊的 undo 歷史條目會被拋棄。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"非線性 Undo","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"非線性 Undo 系統和線性模型大體上是相似的,也會在某處持有一個已經執行的命令的歷史列表。但不同之處在於,用戶在使用 Undo 系統進行反悔時,他可以挑選歷史列表中的某一命令甚至是某一組命令進行 undo,彷佛他並非執行過這些命令一樣。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Adobe Photoshop 在向操作員提供繪圖能力的同時,就維護了一個幾乎接近於非線性的歷史記錄操作表,並且用戶能夠從該列表中挑選一部分予以撤銷/悔改。但 PS 平時在撤銷某一條操作記錄歷史時,其後的更新的操作記錄將被一併回退,從這個角度來看,它還只能算是線性的,只不過運行批處理 undo 罷了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果想要得到非線性撤銷能力,你需要去 PS 的選項中啓用“允許非線性歷史記錄”,這就不再提了。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實上在交互界面上向用戶提供非線性級別的 Undo/Redo 操作能力的應用,通常並沒有誰能很","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"好","attrs":{}},{"type":"text","text":"地支持。一個原因在於從歷史表中抽取一條條目並摘除它,非常容易。但要將這條條目在文檔上的效用抽出來摘除,那就可能是完全辦不到。想象一下,如果你對 pos10..20 做了字體加粗,然後對 pos15..30 做了斜體,然後刪除了 pos16..20 的文字,然後對 pos 13..17 做了字體加大,現在要摘掉對 pos15..30 做的斜體操作。請問,你能夠 Undo 斜體操作這一步嗎?顯然這是相當有難度的:可以有很多中解釋方法來做這筆摘除交易,它們或許都符合編輯者的預期。那個“","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"好","attrs":{}},{"type":"text","text":"”,是個非常主觀的評價級別。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然囉,這也未必就是不能夠實現,從邏輯上來說,單純點,不就是倒退三步,放棄一條操作,然後重播(回放)後繼的兩條操作麼,可能執行起來略有點費內存之外,也不見得一定會是多麼難。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼還得要有另外一個原因在於,很多交互性系統做非線性 Undo 的效果可能是用戶難於腦力預判的,就如我們剛纔舉例的撤銷斜體操作一樣,用戶既然無法預測單獨撤銷一條記錄的後果,那麼這個交互功能提供給他就事實上欠缺了意義——還不如讓他逐步回退呢,這樣他將能精確地把握自己的編輯效用的回退效力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無論是誰最後有道理,都不重要,它們都不會影響到我們做出具備這樣功能的軟件實現。所以實際上有很多類庫能夠提供非線性 Undo 的能力,儘管它們可能並不會被用到某個真實的交互系統上。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此外,關於非線性 Undo 系統的論文也有一大把。充分地證明了論文這種東西,往往都是垃圾——人從出生以來到死去不就是以製造垃圾爲己業的麼。人類認爲多麼燦爛輝煌的文化,對於自然界和宇宙來說,恐怕真的是毫無意義的——直到未來某一天,人類或許能打破壁壘穿行到更高級別的宇宙,脫離與生俱來的本宇宙的藩籬的桎梏,那時候可能過往的一切纔會體現出可能的意義吧。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好,無論宇宙怎麼看,我,仍然認爲現在我製造的新的 memento pattern 的 Non-linear Undo Subsystem 是有意義的,而且將會在下面給你做出展示來。:)","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"進一步的分類","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作爲一個附加的思考,還可以對分類進一步做出組織。在前文的基本劃分之上,還可以進一步做區分:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"有選擇的 Undo","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"可分組的 Undo","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大體上,可選的 Undo 是非線性 Undo 的一種增強的操作體現,它允許用戶在回退歷史操作記錄中勾選某些記錄予以撤銷。而可分租的 Undo 是指命令可以被分組,於是用戶可能必需在已經被分組的操作記錄上整體回退,但這會是 Command Pattern 要去負責管理的事情,只是在 Undo 系統上被體現出來而已。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"C++ 實現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Undo Manager 實現中,可以有一些典型的實現方案:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"將命令模式的每一條命令腳本化。這種方式會設立若干的檢查點,而 Undo 時首先是退到某個檢查點,然後將剩餘的腳本重播一遍,從而完成撤銷某條命令腳本的功能","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"精簡的檢查點。上面的方法,檢查點可能會非常消耗資源,所以有時候需要藉助精緻的命令系統設計來削減檢查點的規模。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"反向播放。這種方式通常只能實現線性回退,其關鍵思想在於反向執行一條命令從而省去建立檢查點的必要。例如最後一步是加粗了 8 個字符,那麼 Undo 時就爲這 8 個字符去掉粗體就行了。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,對於一個元編程實現的通用 Undo 子系統來說,上面提到的方案並不歸屬於 Undo Manager 來管理,它們是劃歸 Command Pattern 去管理的,並且事實上其具體實現由開發者自行完成。Undo Manager 只是負責 states 的存儲、定位和回放等等事務。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"主要設計","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面開始真正介紹 ","attrs":{}},{"type":"link","attrs":{"href":"https;//github.com/hedzr/undo-cxx","title":"","type":null},"content":[{"type":"text","text":"undo-cxx","attrs":{}}]},{"type":"text","text":" 開源庫的實現思路。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"undoable_cmd_system_t","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先還是說主體 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"undoable_cmd_system_t","attrs":{}}],"attrs":{}},{"type":"text","text":",它需要你提供一個主要的模板參數 State。秉承 memento 模式的基本理論,State 指的是你的 Command 所需要保存的狀態包,例如對於編輯器軟件來講,Command 是 FontStyleCmd,表示對選擇文字設定字體樣式,而相應的狀態包可能就包含了對字體樣式的最小描述信息(粗體、斜體等等)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"undoable_cmd_system_t 的宣告大致如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"template,\ntypename BaseCmdT = base_cmd_t,\ntemplate typename RefCmdT = cmd_t,\ntypename Cmd = RefCmdT>\n class undoable_cmd_system_t;\n\ntemplate typename RefCmdT,\ntypename Cmd>\n class undoable_cmd_system_t {\n public:\n ~undoable_cmd_system_t() = default;\n\n using StateT = State;\n using ContextT = Context;\n using CmdT = Cmd;\n using CmdSP = std::shared_ptr;\n using Memento = typename CmdT::Memento;\n using MementoPtr = typename std::unique_ptr;\n // using Container = Stack;\n using Container = std::list;\n using Iterator = typename Container::iterator;\n\n using size_type = typename Container::size_type;\n \n // ...\n };\n\ntemplate,\ntypename BaseCmdT = base_cmd_t,\ntemplate typename RefCmdT = cmd_t,\ntypename Cmd = RefCmdT>\n using MgrT = undoable_cmd_system_t;\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,你所提供的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"State","attrs":{}}],"attrs":{}},{"type":"text","text":" 將被模板參數 Cmd 所使用:","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"typename Cmd = RefCmdT","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"codeinline","content":[{"type":"text","text":"cmd_t","attrs":{}}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而 cmd_t 的宣告是這樣的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"template\nclass cmd_t : public Base {\n public:\n virtual ~cmd_t() {}\n\n using Self = cmd_t;\n using CmdSP = std::shared_ptr;\n using CmdSPC = std::shared_ptr;\n using CmdId = std::string_view;\n CmdId id() const { return debug::type_name(); }\n\n using ContextT = context_t;\n void execute(CmdSP &sender, ContextT &ctx) { do_execute(sender, ctx); }\n\n using StateT = State;\n using StateUniPtr = std::unique_ptr;\n using Memento = state_t;\n using MementoPtr = typename std::unique_ptr;\n MementoPtr save_state(CmdSP &sender, ContextT &ctx) { return save_state_impl(sender, ctx); }\n void undo(CmdSP &sender, ContextT &ctx, Memento &memento) { undo_impl(sender, ctx, memento); }\n void redo(CmdSP &sender, ContextT &ctx, Memento &memento) { redo_impl(sender, ctx, memento); }\n virtual bool can_be_memento() const { return true; }\n\n protected:\n virtual void do_execute(CmdSP &sender, ContextT &ctx) = 0;\n virtual MementoPtr save_state_impl(CmdSP &sender, ContextT &ctx) = 0;\n virtual void undo_impl(CmdSP &sender, ContextT &ctx, Memento &memento) = 0;\n virtual void redo_impl(CmdSP &sender, ContextT &ctx, Memento &memento) = 0;\n};\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也就是說,State 將被我們包裝之後在 undo 系統內部使用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而你應該提供的 Command 類則應該從 cmd_t 派生並實現必要的純虛函數(do_execute, save_state_impl, undo_impl, redo_impl 等等)。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"使用:提供你的命令","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照上面的宣告,我們可以實現一個演示目的的 Command:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"namespace word_processor {\n\n template\n class FontStyleCmd : public undo_cxx::cmd_t {\n public:\n ~FontStyleCmd() {}\n FontStyleCmd() {}\n explicit FontStyleCmd(std::string const &default_state_info)\n : _info(default_state_info) {}\n UNDO_CXX_DEFINE_DEFAULT_CMD_TYPES(FontStyleCmd, undo_cxx::cmd_t);\n\n protected:\n virtual void do_execute(CmdSP &sender, ContextT &) override {\n UNUSED(sender);\n // ... do sth to add/remove font style to/from\n // current selection in current editor ...\n std::cout << \"<>\" << '\\n';\n }\n virtual MementoPtr save_state_impl(CmdSP &sender, ContextT &ctx) override {\n return std::make_unique(sender, _info);\n }\n virtual void undo_impl(CmdSP &sender, ContextT &, Memento &memento) override {\n memento = _info;\n memento.command(sender);\n }\n virtual void redo_impl(CmdSP &sender, ContextT &, Memento &memento) override {\n memento = _info;\n memento.command(sender);\n }\n\n private:\n std::string _info{\"make italic\"};\n };\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在真實的編輯器中,我們相信你有一個所有編輯器窗口的容器並且能跟蹤到當前具有輸入焦點的編輯器。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於此,do_execute 應該是對當前編輯器中的選擇文字做字體樣式設置(如粗體),save_state_impl 應該是將選擇文字的元信息以及 Command 的元信息打包到 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"State","attrs":{}}],"attrs":{}},{"type":"text","text":" 中,undo 應該是反向設置字體樣式(如去掉粗體),redo 應該是依據 memento 的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"State","attrs":{}}],"attrs":{}},{"type":"text","text":" 信息再次設置字體樣式(粗體)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但在本例中,出於演示目的,這些具體細節都被一個 _info 字符串所代表了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘管 FontStyleCmd 保留了 State 模板參數,但演示代碼中 State 只會等於 std::string。","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"使用:提供 UndoCmd 和 RedoCmd","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了定製你的 Undo/Redo 行爲,你可以實現自己的 UndoCmd/RedoCmd。它們需要不同於 cmd_t 的特別的基類:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"namespace word_processor {\n template\n class UndoCmd : public undo_cxx::base_undo_cmd_t {\n public:\n ~UndoCmd() {}\n using undo_cxx::base_undo_cmd_t::base_undo_cmd_t;\n explicit UndoCmd(std::string const &default_state_info)\n : _info(default_state_info) {}\n UNDO_CXX_DEFINE_DEFAULT_CMD_TYPES(UndoCmd, undo_cxx::base_undo_cmd_t);\n\n protected:\n void do_execute(CmdSP &sender, ContextT &ctx) override {\n std::cout << \"<>\" << '\\n';\n Base::do_execute(sender, ctx);\n }\n };\n\n template\n class RedoCmd : public undo_cxx::base_redo_cmd_t {\n public:\n ~RedoCmd() {}\n using undo_cxx::base_redo_cmd_t::base_redo_cmd_t;\n explicit RedoCmd(std::string const &default_state_info)\n : _info(default_state_info) {}\n UNDO_CXX_DEFINE_DEFAULT_CMD_TYPES(RedoCmd, undo_cxx::base_redo_cmd_t);\n\n protected:\n void do_execute(CmdSP &sender, ContextT &ctx) override {\n std::cout << \"<>\" << '\\n';\n Base::do_execute(sender, ctx);\n }\n };\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意對於它們來說,相應的基類被限制爲 base_(undo/redo)_cmd_t ,並且你必需在 do_execute 實現中包含到基類方法的調用,如同這樣:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":" void do_execute(CmdSP &sender, ContextT &ctx) override {\n // std::cout << \"<>\" << '\\n';\n Base::do_execute(sender, ctx);\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基類中有默認的實現,形如這樣:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":" template typename RefCmdT>\n inline void base_redo_cmd_t::\n do_execute(CmdSP &sender, ContextT &ctx) {\n ctx.mgr.redo(sender, Base::_delta);\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它實際上具體地調用 ctx.mgr,也就是 undoable_cmd_system_t 的 redo() 去完成具體的內務,類似的,undo 方面也有相似的語句。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"undo/redo 的特殊之處在於它們的基類有特別的重載函數:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":" virtual bool can_be_memento() const override { return false; }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其目的在於不會考慮該命令的 memento 存檔問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以同時也注意 save_state_impl/undo_impl/redo_impl 是不必要的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"actions_controller","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們現在假定字處理器軟件具有一個命令管理器,它同時也是命令動作的 controller,它將會負責在具體的編輯器窗口中執行一條編輯命令:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"namespace word_processor {\n\n namespace fct = undo_cxx::util::factory;\n\n class actions_controller {\n public:\n using State = std::string;\n using M = undo_cxx::undoable_cmd_system_t;\n\n using UndoCmdT = UndoCmd;\n using RedoCmdT = RedoCmd;\n using FontStyleCmdT = FontStyleCmd;\n\n using Factory = fct::factory<:cmdt undocmdt="" redocmdt="" fontstylecmdt="">;\n\n actions_controller() {}\n ~actions_controller() {}\n\n template\n void invoke(Args &&...args) {\n auto cmd = Factory::make_shared(undo_cxx::id_name(), args...);\n _undoable_cmd_system.invoke(cmd);\n }\n\n template\n void invoke(char const *const cmd_id_name, Args &&...args) {\n auto cmd = Factory::make_shared(cmd_id_name, args...);\n _undoable_cmd_system.invoke(cmd);\n }\n\n void invoke(typename M::CmdSP &cmd) {\n _undoable_cmd_system.invoke(cmd);\n }\n\n private:\n M _undoable_cmd_system;\n };\n\n} // namespace word_processor\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"最後是測試函數","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"藉助於改進過的工廠模式,controller 可以調用編輯命令,注意使用者在發出 undo/redo 時,controller 同樣地通過調用 UndoCmd/RedoCmd 的方式來完成相應的業務邏輯。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"void test_undo_sys() {\n using namespace word_processor;\n actions_controller controller;\n\n using FontStyleCmd = actions_controller::FontStyleCmdT;\n using UndoCmd = actions_controller::UndoCmdT;\n using RedoCmd = actions_controller::RedoCmdT;\n\n // do some stuffs\n\n controller.invoke(\"italic state1\");\n controller.invoke(\"italic-bold state2\");\n controller.invoke(\"underline state3\");\n controller.invoke(\"italic state4\");\n\n // and try to undo or redo\n\n controller.invoke(\"undo 1\");\n controller.invoke(\"undo 2\");\n\n controller.invoke(\"redo 1\");\n\n controller.invoke(\"undo 3\");\n controller.invoke(\"undo 4\");\n\n controller.invoke(\"word_processor::RedoCmd\", \"redo 2 redo\");\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"特性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 undoable_cmd_system_t 的實現中,包含了基本的 Undo/Redo 能力:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無限制的 Undo/Redo","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"受限制的:通過 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"undoable_cmd_system_t::max_size(n)","attrs":{}}],"attrs":{}},{"type":"text","text":" 限制歷史記錄條數","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此外,它是全可定製的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定製你自己的 State 狀態包","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定製你的 context_t 擴展版本以容納自定義對象引用","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果有必要,你可以定製 base_cmd_t 或 cmd_t 來達到你的特別目的","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"分組命令","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過基類 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"class composite_cmd_t","attrs":{}}],"attrs":{}},{"type":"text","text":" 你可以對命令分組,它們在 Undo 歷史記錄中被視爲單條記錄,這允許你批量 Undo/Redo。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了在構造時立即建立組合式命令之外,可以在 composite_cmd_t 的基礎上構造一個 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"class GroupableCmd","attrs":{}}],"attrs":{}},{"type":"text","text":",很容易通過這個類提供運行時就地組合數條命令的能力,這樣,你可以獲得更靈活的命令組。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"受限制的非線性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過批量 Undo/Redo 可以實現受限制的非線性 undo 功能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"undoable_cmd_system_t::erase(n = 1)","attrs":{}}],"attrs":{}},{"type":"text","text":" 能夠刪除當前位置的歷史記錄。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可以認爲 undo i - erase j - redo k 是一種受限制的非線性 undo/redo 實現方式,注意這需要你進一步包裝後再運用(通過爲 UndoCmd/RedoCmd 增加 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"_erased_count","attrs":{}}],"attrs":{}},{"type":"text","text":" 成員並執行 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ctx.mgr.erase(_erased_count)","attrs":{}}],"attrs":{}},{"type":"text","text":" 的方式)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"更全功能的非線性 undo 可能需要一個更復雜的 tree 狀歷史記錄而不是當前的 list,尚須留待將來實現。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"小結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"限於篇幅,不能完整介紹 ","attrs":{}},{"type":"link","attrs":{"href":"https;//github.com/hedzr/undo-cxx","title":"","type":null},"content":[{"type":"text","text":"undo-cxx","attrs":{}}]},{"type":"text","text":" 的能力,所以感興趣的小夥伴直接檢閱 Github 源碼好了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"後記","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一次的 Undo Manager 實現的尚未盡善盡美,以後再找機會改進吧。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"參考:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://en.wikipedia.org/wiki/Memento_pattern","title":"","type":null},"content":[{"type":"text","text":"Memento Pattern - Wiki","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"過段時間再 review,就這麼定了先。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":":end:","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章