精讀《設計模式 - Memoto 備忘錄模式》

Memento(備忘錄模式)

Memento(備忘錄模式)屬於行爲型模式,是針對如何捕獲與恢復對象內部狀態的設計模式。前端

意圖:在不破壞封裝性的前提下,捕獲一個對象的內部狀態,並在該對象以外保存這個狀態。這樣之後就可將該對象恢復到原先保存的狀態。git

其實備忘錄模式思想很是簡單,其核心是定義了一個 Memoto(備忘錄) 封裝對象,由這個對象處理原始對象的狀態捕獲與還原,其餘地方不須要感知其內部數據結構和實現原理,並且 Memoto 對象自己結構也很是簡單,只有 getStatesetState 一存一取兩個方法,後面會詳細講解。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 許可證
相關文章
相關標籤/搜索