Go语言实现GoF设计模式:备忘录模式的实践探索
本文分享自华为云社区《【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
Go语言实现GoF设计模式:备忘录模式的实践探索的更多相关文章
- java设计模式---备忘录模式
一.引子 俗话说:世上难买后悔药.所以凡事讲究个"三思而后行",但总常见有人做"痛心疾首"状:当初我要是--.如果真的有<大话西游>中能时光倒流的& ...
- [转] Android中的设计模式-备忘录模式
转自Android中的设计模式-备忘录模式 定义 备忘录设计模式的定义就是把对象的状态记录和管理委托给外界处理,用以维持自己的封闭性. 比较官方的定义 备忘录模式(Memento Pattern)又叫 ...
- Java设计模式—备忘录模式
个人感觉备忘录模式是一个比较难的设计模式,备忘录模式就是一个对象的备份模式,提供了一种程序数据的备份方法. 定义如下:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态.这样以 ...
- 深入浅出设计模式——备忘录模式(Memento Pattern)
模式动机 为了使软件的使用更加人性化,对于误操作,我们需要提供一种类似“后悔药”的机制,让软件系统可以回到误操作前的状态,因此需要保存用户每一次操作时系统的状态,一旦出现误操作,可以把存储的历史状态取 ...
- IOS设计模式-备忘录模式
内容大纲 如何存储记录 备忘录模式的基本原理 使用备忘录模式 优化存储方案 恢复UIView的状态 1.如何存储记录 在存储记录时,第一步我们需要用一把钥匙去打开一把锁.第二步,当我们打开锁之后就会有 ...
- C++设计模式——备忘录模式
备忘录模式 在GOF的<设计模式:可复用面向对象软件的基础>一书中对备忘录模式是这样说的:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态.这样以后就可将该对象恢 ...
- JAVA 设计模式 备忘录模式
用途 备忘录模式 (Memento) 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态. 这样以后就可将该对象恢复到原先保存的状态. 备忘录模式是一种行为型模式. 结构
- [工作中的设计模式]备忘录模式memento
一.模式解析 备忘录对象是一个用来存储另外一个对象内部状态的快照的对象.备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉(Capture)住,并外部化,存储起来,从而可以在将来合适的时候把 ...
- PHP设计模式——备忘录模式
声明:本系列博客參考资料<大话设计模式>,作者程杰. 备忘录模式又叫做快照模式或Token模式,在不破坏封闭的前提下.捕获一个对象的内部状态,并在该对象之外保存这个状态.这样以后就可将该对 ...
- C#设计模式-备忘录模式
访问者模式的实现是把作用于某种数据结构上的操作封装到访问者中,使得操作和数据结构隔离.而本文要介绍的备忘者模式与命令模式有点相似,不同的是,命令模式保存的是发起人的具体命令(命令对应的是行为),而备忘 ...
随机推荐
- Docker 安装 Redis 单机&集群总结
前言 Redis 是一个开源的使用 ANSI C 语言编写.遵守 BSD 协议.支持网络.可基于 内存 . 分布式 .可选持久性的键值对( Key-Value )存储数据库 redis版本:redis ...
- Gossip in Hyperledger Fabric
1. Gossip协议基础 1.1 什么是分布式系统 分布式系统(Distributed System)是由多台计算机或计算节点组成的计算机系统,这些计算节点通过网络连接在一起,并协同工作以完成共同的 ...
- Jmeter调用Python脚本,实现参数互相传递
首先传值到python 1:新增取样器--->os进程取样器 2:新建.bat文件 (写python脚本的路径和传参的个数:思路是:jmeter调用shell,用shell执行py) F:cd ...
- Chrome Extensions v3 迁移清单
一.前置问题 1.1为什么需要迁移 v3? Chrome 计划完全停止 v2 版本维护,后续 v2 版本将无法上架谷歌插件商店,除此之外,未来新版本 Chrome 对于 v2 版本插件的限制会越来越大 ...
- Linux第一次周总结
第一章 初识 Linux 1.Linux简介 Linux,一般指GNU/Linux(单独的Linux内核并不可直接使用,一般搭配GNU套件,故得此称呼),是一种免费使用和自由传播的类UNIX操作系统 ...
- CF276C
题目简化和分析: 属于一种贪心思维,我们想如果要使得和最大,那么就必须保证最大的数乘的次数越多越好,并且排序没有限制,快速累加每个位置出现的次数,所以应该使用线段树差分. 然后排序最大乘最大累加. S ...
- 每天5分钟复习OpenStack(四) virsh 常用命令
在上一章节中,我们拉起了第一台虚拟机,但是执行virsh shutdown 关机是无法关机的,需要使用virsh destroy 强制断电的命令来关机.为什么会这样了? 这里我们介绍下 QGA的概念 ...
- 《最新出炉》系列初窥篇-Python+Playwright自动化测试-22-处理select下拉框-上篇
1.简介 在实际自动化测试过程中,我们也避免不了会遇到下拉框选择的测试,因此宏哥在这里直接分享和介绍一下,希望小伙伴或者童鞋们在以后工作中遇到可以有所帮助.今天,我们讲下playwright的下拉框怎 ...
- 前后端分离,前端发送过来的请求是服务器的ip还是用户的ip
前后端分离部署时,服务器A用于部署前端项目,称为前端服务器,服务器B用于部署后端项目,称为后端服务器.后端服务器通过开放API的方式,向前端服务器中的前端项目提供数据或数据操作接口,以此实现前端与后端 ...
- 彻底掌握Python中 * 号
Python中的 *号是一个特殊的符号,在其他编程语言中,它最广为人知的用途就是作为乘法运算的符号.而在Python中,它的用途远不止如此. 本文总结了Python中*号的所有用途,以供参考. 1. ...