本文分享自华为云社区《【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规划形式:备忘录形式的实践探究

场景上下文

在前文 【Go完成】实践GoF的23种规划形式:指令形式 咱们说到,在 简略的分布式使用体系(示例代码工程)中,db 模块用来存储服务注册信息和体系监控数据。其间,服务注册信息拆成了 profilesregions 两个表,在服务发现的业务逻辑中,通常需求同时操作两个表,为了避免两个表数据不一致的问题,db 模块需求供给业务功用:

Go言语完成GoF规划形式:备忘录形式的实践探究

业务的中心功用之一是,当其间某个句子履行失利时,之前已履行成功的句子可以回滚,前文咱们现已介绍如何依据 指令形式 搭建业务框架,下面咱们将重点介绍,如何依据备忘录形式完成失利回滚的功用。

代码完成

// 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 进行完成,但本质是相同的,总结起来有以下几个要害点:

  1. 界说笼统备忘录 Memento 接口,这儿为 Command 接口。Command 的完成是详细的数据库履行操作,而且存有对应的回滚操作,比方 InsertCmd 为“刺进”操作,其对应的回滚操作为“删去”,咱们保存的状况便是“删去”这一回滚操作。
  2. 界说 Originator 结构体/接口,这儿为 Db 接口。备忘录 Command 记载的便是它的状况。
  3. 界说 Caretaker 结构体/接口,这儿为 Transaction 结构体。Transaction 采用了延迟履行的规划,当调用 Exec 办法时只会将指令缓存到 cmds 行列中,比及调用 Commit 办法时才会履行。
  4. 在 Caretaker 中引证 Originator 目标,用于后续对其状况的保存和康复。这儿为 Transaction 聚合了 Db
  5. 在 Caretaker 中界说备忘录列表,用于保存某一时间的体系状况。这儿为在 Transaction.Commit 办法中界说了 cmdHistory 目标,保存一直履行成功的 Command
  6. 履行 Caretaker 详细的业务逻辑,这儿为在 Transaction.Commit 中调用 Command.Exec 办法,履行详细的数据库操作指令。
  7. 业务逻辑履行成功后,保存当时的状况。这儿为调用 cmdHistory.add 办法将 Command 保存起来。
  8. 假如业务逻辑履行失利,则康复到原来的状况。这儿为调用cmdHistory.rollback 办法,反向履行已履行成功的 CommandUndo 办法进行状况康复。
  9. 依据详细的业务需求,界说详细的备忘录,这儿界说了InsertCmdUpdateCmdDeleteCmd

扩展

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 这类的文档编辑软件的吊销功用。

优缺点

优点

  1. 供给了一种状况康复的机制,让体系可以方便地回到某个特定状况下。
  2. 完成了对状况的封装,可以在不损坏封装的前提下完成状况的保存和康复。

缺点

  1. 资源耗费大。体系状况的保存意味着存储空间的耗费,本质上是空间换时间的战略。

    undo log 是一种折中方案,保存的状况并非某一时间数据库的所有数据,而是一条反操作的 SQL 句子,存储空间大大减少。

  2. 并发安全。在多线程场景,完成备忘录形式时,要留意在保证状况的不变性,不然可能会有并发安全问题。

与其他形式的关联

在完成 Undo/Redo 操作时,你通常需求同时使用 备忘录形式指令形式

别的,当你需求遍历备忘录目标中的成员时,通常会使用 迭代器形式,以防损坏目标的封装。

文章配图

可以在 用Keynote画出手绘风格的配图 中找到文章的绘图办法。

参阅

点击关注,第一时间了解华为云新鲜技术~