本文分享自华为云社区《【Go完成】实践GoF的23种规划形式:备忘录形式》,作者:元闰子。
简介
相对于署理形式、工厂形式等规划形式,备忘录形式(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
package db
// Command 履行数据库操作的指令接口,同时也是备忘录接口
// 要害点1:界说Memento接口,其间Exec办法相当于UML图中的SetState办法,调用后会将状况保存至Db中
type Command interface {
Exec() error // Exec 履行insert、update、delete指令
Undo() // Undo 回滚指令
setDb(db Db) // SetDb 设置关联的数据库
}
// 要害点2:界说Originator,在本比方中,状况都是存储在Db目标中
type Db interface {...}
// Transaction Db业务完成,业务接口的调用次序为begin -> exec -> exec > ... -> commit
// 要害点3:界说Caretaker,Transaction里完成了对句子的履行(Do)和回滚(Undo)操作
type Transaction struct {
name string
// 要害点4:在Caretaker(Transaction)中引证Originator(Db)目标,用于后续对其状况的保存和康复
db Db
// 留意,这儿的cmds并非备忘录列表,真实的history在Commit办法中
cmds []Command
}
// Begin 敞开一个业务
func (t *Transaction) Begin() {
t.cmds = make([]Command, 0)
}
// Exec 在业务中履行指令,先缓存到cmds行列中,等commit时再履行
func (t *Transaction) Exec(cmd Command) error {
if t.cmds == nil {
return ErrTransactionNotBegin
}
cmd.setDb(t.db)
t.cmds = append(t.cmds, cmd)
return nil
}
// Commit 提交业务,履行行列中的指令,假如有指令失利,则回滚后回来错误
func (t *Transaction) Commit() error {
// 要害点5:界说备忘录列表,用于保存某一时间的体系状况
history := &cmdHistory{history: make([]Command, 0, len(t.cmds))}
for _, cmd := range t.cmds {
// 要害点6:履行Do办法
if err := cmd.Exec(); err != nil {
// 要害点8:当Do办法履行失利时,则进行Undo操作,依据备忘录history中的状况进行回滚
history.rollback()
return err
}
// 要害点7:假如Do办法履行成功,则将状况(cmd)保存在备忘录history中
history.add(cmd)
}
return nil
}
// cmdHistory 指令履行前史
type cmdHistory struct {
history []Command
}
func (c *cmdHistory) add(cmd Command) {
c.history = append(c.history, cmd)
}
func (c *cmdHistory) rollback() {
for i := len(c.history) - 1; i >= 0; i-- {
c.history[i].Undo()
}
}
// InsertCmd 刺进指令
// 要害点9: 界说详细的备忘录类,完成Memento接口
type InsertCmd struct {
db Db
tableName string
primaryKey interface{}
newRecord interface{}
}
func (i *InsertCmd) Exec() error {
return i.db.Insert(i.tableName, i.primaryKey, i.newRecord)
}
func (i *InsertCmd) Undo() {
i.db.Delete(i.tableName, i.primaryKey)
}
func (i *InsertCmd) setDb(db Db) {
i.db = db
}
// UpdateCmd 更新指令
type UpdateCmd struct {...}
// DeleteCmd 删去指令
type DeleteCmd struct {...}
客户端可以这么使用:
func client() {
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)
if err := 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 来回滚业务,即康复原先的记载值。
比方,履行一条刺进句子:
insert into region(id, name) values (1, "beijing");
那么,写入到 undo log 中对应的回滚句子为:
delete from region where id = 1;
当履行一条句子失利,需求回滚时,MySQL 就会从读取对应的回滚句子来履行,从而将数据康复至业务提交之前的状况。undo log 是 MySQL 完成业务回滚和多版别控制(MVCC)的根基。
典型使用场景
- 业务回滚。业务回滚的一种常见完成办法是 undo log,其本质上用的便是备忘录形式。
- 体系快照(Snapshot)。多版别控制的用法,保存某一时间的体系状况快照,以便在将来可以康复。
- 吊销功用。比方 Microsoft Offices 这类的文档编辑软件的吊销功用。
优缺点
优点
- 供给了一种状况康复的机制,让体系可以方便地回到某个特定状况下。
- 完成了对状况的封装,可以在不损坏封装的前提下完成状况的保存和康复。
缺点
-
资源耗费大。体系状况的保存意味着存储空间的耗费,本质上是空间换时间的战略。
undo log 是一种折中方案,保存的状况并非某一时间数据库的所有数据,而是一条反操作的 SQL 句子,存储空间大大减少。
-
并发安全。在多线程场景,完成备忘录形式时,要留意在保证状况的不变性,不然可能会有并发安全问题。
与其他形式的关联
在完成 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