簡介
相對于代理模式、工廠模式等設計模式,備忘錄模式(Memento)在我們日常開發中出鏡率并不高,除了應用場景的限制之外,另一個原因,可能是備忘錄模式 UML 結構的幾個概念比較晦澀難懂,難以映射到代碼實現中。比如 Originator(原發器)和 Caretaker(負責人),從字面上很難看出它們在模式中的職責。
但從定義來看,備忘錄模式又是簡單易懂的,GoF 對備忘錄模式的定義如下:
Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.
也即,在不破壞封裝的前提下,捕獲一個對象的內部狀態,并在該對象之外進行保存,以便在未來將對象恢復到原先保存的狀態。
從定義上看,備忘錄模式有幾個關鍵點:封裝、保存、恢復。
對狀態的封裝,主要是為了未來狀態修改或擴展時,不會引發霰彈式修改;保存和恢復則是備忘錄模式的主要特點,能夠對當前對象的狀態進行保存,并能夠在未來某一時刻恢復出來。
現在,在回過頭來看備忘錄模式的 3 個角色就比較好理解了:
Memento(備忘錄):是對狀態的封裝,可以是struct,也可以是interface。
Originator(原發器):備忘錄的創建者,備忘錄里存儲的就是 Originator 的狀態。
Caretaker(負責人):負責對備忘錄的保存和恢復,無須知道備忘錄中的實現細節。
UML 結構
場景上下文
在前文【Go實現】實踐GoF的23種設計模式:命令模式我們提到,在簡單的分布式應用系統(示例代碼工程)中,db 模塊用來存儲服務注冊信息和系統監控數據。其中,服務注冊信息拆成了profiles和regions兩個表,在服務發現的業務邏輯中,通常需要同時操作兩個表,為了避免兩個表數據不一致的問題,db 模塊需要提供事務功能:
事務的核心功能之一是,當其中某個語句執行失敗時,之前已執行成功的語句能夠回滾,前文我們已經介紹如何基于命令模式搭建事務框架,下面我們將重點介紹,如何基于備忘錄模式實現失敗回滾的功能。
代碼實現
//demo/db/transaction.go packagedb //Command執行數據庫操作的命令接口,同時也是備忘錄接口 //關鍵點1:定義Memento接口,其中Exec方法相當于UML圖中的SetState方法,調用后會將狀態保存至Db中 typeCommandinterface{ Exec()error//Exec執行insert、update、delete命令 Undo()//Undo回滾命令 setDb(dbDb)//SetDb設置關聯的數據庫 } //關鍵點2:定義Originator,在本例子中,狀態都是存儲在Db對象中 typeDbinterface{...} //TransactionDb事務實現,事務接口的調用順序為begin->exec->exec>...->commit //關鍵點3:定義Caretaker,Transaction里實現了對語句的執行(Do)和回滾(Undo)操作 typeTransactionstruct{ namestring //關鍵點4:在Caretaker(Transaction)中引用Originator(Db)對象,用于后續對其狀態的保存和恢復 dbDb //注意,這里的cmds并非備忘錄列表,真正的history在Commit方法中 cmds[]Command } //Begin開啟一個事務 func(t*Transaction)Begin(){ t.cmds=make([]Command,0) } //Exec在事務中執行命令,先緩存到cmds隊列中,等commit時再執行 func(t*Transaction)Exec(cmdCommand)error{ ift.cmds==nil{ returnErrTransactionNotBegin } cmd.setDb(t.db) t.cmds=append(t.cmds,cmd) returnnil } //Commit提交事務,執行隊列中的命令,如果有命令失敗,則回滾后返回錯誤 func(t*Transaction)Commit()error{ //關鍵點5:定義備忘錄列表,用于保存某一時刻的系統狀態 history:=&cmdHistory{history:make([]Command,0,len(t.cmds))} for_,cmd:=ranget.cmds{ //關鍵點6:執行Do方法 iferr:=cmd.Exec();err!=nil{ //關鍵點8:當Do方法執行失敗時,則進行Undo操作,根據備忘錄history中的狀態進行回滾 history.rollback() returnerr } //關鍵點7:如果Do方法執行成功,則將狀態(cmd)保存在備忘錄history中 history.add(cmd) } returnnil } //cmdHistory命令執行歷史 typecmdHistorystruct{ history[]Command } func(c*cmdHistory)add(cmdCommand){ c.history=append(c.history,cmd) } func(c*cmdHistory)rollback(){ fori:=len(c.history)-1;i>=0;i--{ c.history[i].Undo() } } //InsertCmd插入命令 //關鍵點9:定義具體的備忘錄類,實現Memento接口 typeInsertCmdstruct{ dbDb tableNamestring primaryKeyinterface{} newRecordinterface{} } func(i*InsertCmd)Exec()error{ returni.db.Insert(i.tableName,i.primaryKey,i.newRecord) } func(i*InsertCmd)Undo(){ i.db.Delete(i.tableName,i.primaryKey) } func(i*InsertCmd)setDb(dbDb){ i.db=db } //UpdateCmd更新命令 typeUpdateCmdstruct{...} //DeleteCmd刪除命令 typeDeleteCmdstruct{...}
客戶端可以這么使用:
funcclient(){ transaction:=db.CreateTransaction("register"+profile.Id) transaction.Begin() rcmd:=db.NewUpdateCmd(regionTable).WithPrimaryKey(profile.Region.Id).WithRecord(profile.Region) transaction.Exec(rcmd) pcmd:=db.NewUpdateCmd(profileTable).WithPrimaryKey(profile.Id).WithRecord(profile.ToTableRecord()) transaction.Exec(pcmd) iferr:=transaction.Commit();err!=nil{ return... } return... }
這里并沒有完全按照標準的備忘錄模式 UML 進行實現,但本質是一樣的,總結起來有以下幾個關鍵點:
定義抽象備忘錄 Memento 接口,這里為Command接口。Command的實現是具體的數據庫執行操作,并且存有對應的回滾操作,比如InsertCmd為“插入”操作,其對應的回滾操作為“刪除”,我們保存的狀態就是“刪除”這一回滾操作。
定義 Originator 結構體/接口,這里為Db接口。備忘錄Command記錄的就是它的狀態。
定義 Caretaker 結構體/接口,這里為Transaction結構體。Transaction采用了延遲執行的設計,當調用Exec方法時只會將命令緩存到cmds隊列中,等到調用Commit方法時才會執行。
在 Caretaker 中引用 Originator 對象,用于后續對其狀態的保存和恢復。這里為Transaction聚合了Db。
在 Caretaker 中定義備忘錄列表,用于保存某一時刻的系統狀態。這里為在Transaction.Commit方法中定義了cmdHistory對象,保存一直執行成功的Command。
執行 Caretaker 具體的業務邏輯,這里為在Transaction.Commit中調用Command.Exec方法,執行具體的數據庫操作命令。
業務邏輯執行成功后,保存當前的狀態。這里為調用cmdHistory.add方法將Command保存起來。
如果業務邏輯執行失敗,則恢復到原來的狀態。這里為調用cmdHistory.rollback方法,反向執行已執行成功的Command的Undo方法進行狀態恢復。
根據具體的業務需要,定義具體的備忘錄,這里定義了InsertCmd、UpdateCmd和DeleteCmd。
擴展
MySQL 的 undo log 機制
MySQL 的undo log(回滾日志)機制本質上用的就是備忘錄模式的思想,前文中Transaction回滾機制實現的方法參考的就是 undo log 機制。
undo log 原理是,在提交事務之前,會把該事務對應的回滾操作(狀態)先保存到 undo log 中,然后再提交事務,當出錯的時候 MySQL 就可以利用 undo log 來回滾事務,即恢復原先的記錄值。
比如,執行一條插入語句:
insertintoregion(id,name)values(1,"beijing");
那么,寫入到 undo log 中對應的回滾語句為:
deletefromregionwhereid=1;
當執行一條語句失敗,需要回滾時,MySQL 就會從讀取對應的回滾語句來執行,從而將數據恢復至事務提交之前的狀態。undo log 是 MySQL 實現事務回滾和多版本控制(MVCC)的根基。
典型應用場景
事務回滾。事務回滾的一種常見實現方法是 undo log,其本質上用的就是備忘錄模式。
系統快照(Snapshot)。多版本控制的用法,保存某一時刻的系統狀態快照,以便在將來能夠恢復。
撤銷功能。比如 Microsoft Offices 這類的文檔編輯軟件的撤銷功能。
優缺點
優點
提供了一種狀態恢復的機制,讓系統能夠方便地回到某個特定狀態下。
實現了對狀態的封裝,能夠在不破壞封裝的前提下實現狀態的保存和恢復。
缺點
資源消耗大。系統狀態的保存意味著存儲空間的消耗,本質上是空間換時間的策略。undo log 是一種折中方案,保存的狀態并非某一時刻數據庫的所有數據,而是一條反操作的 SQL 語句,存儲空間大大減少。
并發安全。在多線程場景,實現備忘錄模式時,要注意在保證狀態的不變性,否則可能會有并發安全問題。
與其他模式的關聯
在實現 Undo/Redo 操作時,你通常需要同時使用備忘錄模式與命令模式。
另外,當你需要遍歷備忘錄對象中的成員時,通常會使用迭代器模式,以防破壞對象的封裝。
文章配圖
可以在用Keynote畫出手繪風格的配圖中找到文章的繪圖方法。
審核編輯:劉清
-
UML
+關注
關注
0文章
122瀏覽量
30858 -
MySQL
+關注
關注
1文章
804瀏覽量
26528 -
MVCC
+關注
關注
0文章
13瀏覽量
1465
原文標題:【Go實現】實踐GoF的23種設計模式:備忘錄模式
文章出處:【微信號:yuanrunzi,微信公眾號:元閏子的邀請】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論