Memento(備忘錄模式)屬於行爲型模式,是針對如何捕獲與恢復對象內部狀態的設計模式。前端
意圖:在不破壞封裝性的前提下,捕獲一個對象的內部狀態,並在該對象以外保存這個狀態。這樣之後就可將該對象恢復到原先保存的狀態。git
其實備忘錄模式思想很是簡單,其核心是定義了一個 Memoto(備忘錄) 封裝對象,由這個對象處理原始對象的狀態捕獲與還原,其餘地方不須要感知其內部數據結構和實現原理,並且 Memoto 對象自己結構也很是簡單,只有 getState
與 setState
一存一取兩個方法,後面會詳細講解。github
若是看不懂上面的意圖介紹,沒有關係,設計模式須要在平常工做裏用起來,結合例子能夠加深你的理解,下面我準備了三個例子,讓你體會什麼場景下會用到這種設計模式。typescript
若是撤銷重作涉及到大量複雜對象,每一個對象內部狀態的存儲結構都不一樣,若是一個一個處理,很容易寫出 case by case 的冗餘代碼,並且在拓展一種新對象結構時(如嵌入 ppt),還須要在撤銷重作時對相應結構作處理。備忘錄思惟至關於一種統一封裝思惟,無論這個對象結構如何,均可以保存在一個 Memoto 對象中,經過 setState
設置對象狀態與 getState
獲取對象狀態,這樣對於任何類型的對象,畫布均可以經過統一的 API 操做進行存取了。編程
玩過遊戲的同窗都知道,許多遊戲支持設置與讀取多種存檔,若是轉換爲代碼模式,咱們可能但願有這樣一種 API 進行多存檔管理:redux
// 建立一盤遊戲。 const game = new Game() // 玩一會。 game.play() // 設置一個存檔(archive) 1。 const gameArchive1 = game.createArchive() // 再玩一會。 game.play() // 設置一個存檔(archive) 2。 const gameArchive2 = game.createArchive() // 再玩一會。 game.play() // 這個時候角色掛了,提示 「請讀取存檔」,玩家此時選擇了存檔 1。 game.loadArchive(gameArchive1) // 此時遊戲恢復存檔 1 狀態,又能夠愉快的玩耍了。
其實在遊戲保存的例子中,存檔就是備忘錄(Memoto),而主進程管理遊戲狀態時,只是簡單調用了 createArchive
建立存檔,與 load
讀取存檔,便可實現複雜的遊戲保存與讀取功能,全程是不須要關心遊戲內部狀態到底有多少,以及這麼多狀態須要如何一一恢復的,這就是得益於備忘錄模式的設計。設計模式
富文本編輯器的文檔草稿保存也是同樣的原理,簡單一點只須要一個 Memoto 對象便可,若是要實現複雜一點的多版本狀態管理,只須要相似遊戲保存機制,存儲多個 Memoto 存檔便可。數組
看到這裏,會發現備忘錄模式與前端狀態管理的保存與恢復很像。以 Redux 類比:微信
setState
就像 reducer
處理的最終 state
狀態同樣,對 redux 全局狀態來講,它不用關心業務邏輯(有多少 reducer
,以及每一個 reducer
作了什麼),它只須要知道任何 reducer
最後處理完後都是一個 state
對象,將其生成出來並存下來便可。數據結構
恢復也是同樣,initState
就相似 getState
,只要將上一次生成的 state
灌進來,就能夠徹底還原某個時刻的狀態,而不須要關心這個狀態內部是怎樣的。
因此其實備忘錄模式早已獲得普遍的應用,仔細去理解後,會發現不必去扣的太細,以及原始設計模式是如何定義的,由於通過幾十年的演化,這些設計模式思路早已融入了編程框架的方方面面。
但依照慣例,咱們仍是再咬文嚼字解釋一下意圖:
意圖:在不破壞封裝性的前提下,捕獲一個對象的內部狀態,並在該對象以外保存這個狀態。這樣之後就可將該對象恢復到原先保存的狀態。
重點在於 「不破壞封裝性」 這幾個字上,程序的可維護性永遠是設計模式關注的重點,不管是遊戲存檔的例子,仍是 Redux 的例子,上層框架使用狀態時,都不須要知道具體對象狀態的細節,而實現這一點的就是 Memoto 這個抽象的備忘錄類。
Originator
:建立、讀取備忘錄的發起者。Memento
:備忘錄,專門存儲原始對象狀態,而且防止 Originator 以外的對象讀取。Caretaker
:備忘錄管理者,通常用數組或鏈表管理一堆備忘錄,在撤銷重作或者版本管理時會用到。下面例子使用 typescript 編寫。
下面是備忘錄模式三劍客的定義:
// 備忘錄 class Memento { public state: any constructor(state: any) { this.state = state } public getState() { return this.state } } // 備忘錄管理者 class Caretaker { private stack: Memento[] = [] public getMemento(){ return this.stack.pop() } public addMemento(memoto: Memento){ this.stack.push(memoto) } } // 發起者 class Originator { private state: any public getState() { return this.state } public setState(state: any) { this.state = state } public createMemoto() { return new Memoto(this.state) } public setMemoto(memoto: Memoto) { this.state = memoto.getState() } public void setMemento(Memento memento) { state = memento.getState(); } }
下面是一個簡化版客戶端使用的例子:
// 實例化發起者,好比畫布、文章管理器、遊戲管理器 const originator = new Originator() // 實例化備忘錄管理者 const caretaker = new Caretaker() // 設置狀態,分別對應: // 畫布的組件操做。 // 文章的輸入。 // 遊戲的 .play() originator.setState('hello world') // 備忘錄管理者記錄一次狀態,分別對應: // 畫布的保存。 // 文章的保存。 // 遊戲的保存。 caretaker.setMemento(originator.createMento()) // 從備忘錄管理者還原狀態,分別對應: // 畫布的還原。 // 文章的讀取。 // 遊戲讀取存檔。 originator.setMemento(caretaker.getMemento())
在上面例子中,備忘錄管理者存儲狀態是數組,因此能夠實現撤銷重作,若是要實現任意讀檔,能夠將備忘錄變爲 Map
結構,按照 key
來讀取,若是沒有這些要求,存一個單一的 Memoto
也夠用了。
備忘錄模式存儲的是完整狀態而非 Diff,因此可能會在運行時消耗大量內存(固然在 Immutable 模式下,經過引用共享能夠極大程度緩解這個問題)。
另外就是,備忘錄模式已經很大程度上被融合到現代框架中,你在使用狀態管理工具時就已經使用了備忘錄模式了,因此不少狀況下,不須要機械的按照上面的代碼例子使用。設計模式重點在於利用它優化了程序的可維護性,而不用強求使用方式和官方描述如出一轍。
備忘錄模式經過備忘錄對象,將對象內部狀態封裝了起來,簡化了程序複雜度,這符合設計模式一向遵循的 「高內聚、低耦合」 原則。
其實踐行備忘錄模式最好的例子就是 Redux,當項目全部狀態都使用 Redux 管理時,你會發現不管是撤銷重作,仍是保存讀取,均可以很是輕鬆完成,這時候,不要質疑爲何備忘錄模式還在解決這種 「遇不到的問題」,由於 Redux 自己就包含了備忘錄設計模式的理念。
討論地址是: 精讀《設計模式 - Memento 備忘錄模式》· Issue #301 · dt-fe/weekly
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
版權聲明:自由轉載-非商用-非衍生-保持署名( 創意共享 3.0 許可證)