上一篇:【Go实现】实践GoF的23种设计模式:命令模式 简单的分布式应用系统(示例代码工程):https://github.com/ruanrunxue/Practice-Design-Pattern--Go-Implementation
相对于代理模式、工厂模式等设计模式,备忘录模式(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 个角色就比较好理解了:
struct
,也可以是 interface
。在前文 【Go实现】实践GoF的23种设计模式:命令模式 我们提到,在 简单的分布式应用系统(示例代码工程)中,db 模块用来存储服务注册信息和系统监控数据。其中,服务注册信息拆成了 profiles
和 regions
两个表,在服务发现的业务逻辑中,通常需要同时操作两个表,为了避免两个表数据不一致的问题,db 模块需要提供事务功能:
事务的核心功能之一是,当其中某个语句执行失败时,之前已执行成功的语句能够回滚,前文我们已经介绍如何基于 命令模式 搭建事务框架,下面我们将重点介绍,如何基于备忘录模式实现失败回滚的功能。
1// demo/db/transaction.go2package db34// Command 执行数据库操作的命令接口,同时也是备忘录接口5// 关键点1:定义Memento接口,其中Exec方法相当于UML图中的SetState方法,调用后会将状态保存至Db中6type Command interface {7 Exec() error // Exec 执行insert、update、delete命令8 Undo() // Undo 回滚命令9 setDb(db Db) // SetDb 设置关联的数据库10}1112// 关键点2:定义Originator,在本例子中,状态都是存储在Db对象中13type Db interface {...}1415// Transaction Db事务实现,事务接口的调用顺序为begin -> exec -> exec > ... -> commit16// 关键点3:定义Caretaker,Transaction里实现了对语句的执行(Do)和回滚(Undo)操作17type Transaction struct {18 name string19 // 关键点4:在Caretaker(Transaction)中引用Originator(Db)对象,用于后续对其状态的保存和恢复20 db Db21 // 注意,这里的cmds并非备忘录列表,真正的history在Commit方法中22 cmds []Command 23}24// Begin 开启一个事务25func (t *Transaction) Begin() {26 t.cmds = make([]Command, 0)27}28// Exec 在事务中执行命令,先缓存到cmds队列中,等commit时再执行29func (t *Transaction) Exec(cmd Command) error {30 if t.cmds == nil {31 return ErrTransactionNotBegin32 }33 cmd.setDb(t.db)34 t.cmds = append(t.cmds, cmd)35 return nil36}37// Commit 提交事务,执行队列中的命令,如果有命令失败,则回滚后返回错误38func (t *Transaction) Commit() error {39 // 关键点5:定义备忘录列表,用于保存某一时刻的系统状态40 history := &cmdHistory{history: make([]Command, 0, len(t.cmds))}41 for _, cmd := range t.cmds {42 // 关键点6:执行Do方法43 if err := cmd.Exec(); err != nil {44 // 关键点8:当Do方法执行失败时,则进行Undo操作,根据备忘录history中的状态进行回滚45 history.rollback()46 return err47 }48 // 关键点7:如果Do方法执行成功,则将状态(cmd)保存在备忘录history中49 history.add(cmd)50 }51 return nil52}53// cmdHistory 命令执行历史54type cmdHistory struct {55 history []Command56}57func (c *cmdHistory) add(cmd Command) {58 c.history = append(c.history, cmd)59}6061func (c *cmdHistory) rollback() {62 for i := len(c.history) - 1; i >= 0; i-- {63 c.history[i].Undo()64 }65}6667// InsertCmd 插入命令68// 关键点9: 定义具体的备忘录类,实现Memento接口69type InsertCmd struct {70 db Db71 tableName string72 primaryKey interface{}73 newRecord interface{}74}7576func (i *InsertCmd) Exec() error {77 return i.db.Insert(i.tableName, i.primaryKey, i.newRecord)78}79func (i *InsertCmd) Undo() {80 i.db.Delete(i.tableName, i.primaryKey)81}82func (i *InsertCmd) setDb(db Db) {83 i.db = db84}8586// UpdateCmd 更新命令87type UpdateCmd struct {...}88// DeleteCmd 删除命令89type DeleteCmd struct {...}
客户端可以这么使用:
1func client() {2 transaction := db.CreateTransaction("register" + profile.Id)3 transaction.Begin()4 rcmd := db.NewUpdateCmd(regionTable).WithPrimaryKey(profile.Region.Id).WithRecord(profile.Region)5 transaction.Exec(rcmd)6 pcmd := db.NewUpdateCmd(profileTable).WithPrimaryKey(profile.Id).WithRecord(profile.ToTableRecord())7 transaction.Exec(pcmd)8 if err := transaction.Commit(); err != nil {9 return ... 10 }11 return ...12}
这里并没有完全按照标准的备忘录模式 UML 进行实现,但本质是一样的,总结起来有以下几个关键点:
Command
接口。Command
的实现是具体的数据库执行操作,并且存有对应的回滚操作,比如 InsertCmd
为“插入”操作,其对应的回滚操作为“删除”,我们保存的状态就是“删除”这一回滚操作。Db
接口。备忘录 Command
记录的就是它的状态。Transaction
结构体。Transaction
采用了延迟执行的设计,当调用 Exec
方法时只会将命令缓存到 cmds
队列中,等到调用 Commit
方法时才会执行。Transaction
聚合了 Db
。Transaction.Commit
方法中定义了 cmdHistory
对象,保存一直执行成功的 Command
。Transaction.Commit
中调用 Command.Exec
方法,执行具体的数据库操作命令。cmdHistory.add
方法将 Command
保存起来。cmdHistory.rollback
方法,反向执行已执行成功的 Command
的 Undo
方法进行状态恢复。InsertCmd
、UpdateCmd
和 DeleteCmd
。MySQL 的 undo log(回滚日志)机制本质上用的就是备忘录模式的思想,前文中 Transaction
回滚机制实现的方法参考的就是 undo log 机制。
undo log 原理是,在提交事务之前,会把该事务对应的回滚操作(状态)先保存到 undo log 中,然后再提交事务,当出错的时候 MySQL 就可以利用 undo log 来回滚事务,即恢复原先的记录值。
比如,执行一条插入语句:
1insert into region(id, name) values (1, "beijing");
那么,写入到 undo log 中对应的回滚语句为:
1delete from region where id = 1;
当执行一条语句失败,需要回滚时,MySQL 就会从读取对应的回滚语句来执行,从而将数据恢复至事务提交之前的状态。undo log 是 MySQL 实现事务回滚和多版本控制(MVCC)的根基。
在实现 Undo/Redo 操作时,你通常需要同时使用 备忘录模式 与 命令模式。
另外,当你需要遍历备忘录对象中的成员时,通常会使用 迭代器模式,以防破坏对象的封装。
可以在 用Keynote画出手绘风格的配图 中找到文章的绘图方法。
参考 [1] 【Go实现】实践GoF的23种设计模式:SOLID原则, 元闰子 [2] 【Go实现】实践GoF的23种设计模式:命令模式, 元闰子 [3] Design Patterns, Chapter 5. Behavioral Patterns, GoF [4] 备忘录模式, refactoringguru.cn [5] MySQL 8.0 Reference Manual :: 15.6.6 Undo Logs, MySQL 更多文章请关注微信公众号:元闰子的邀请
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。