【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 个角色就比较好理解了:
Memento(备忘录):是对状态的封装,可以是
struct
,也可以是interface
。Originator(原发器):备忘录的创建者,备忘录里存储的就是 Originator 的状态。
Caretaker(负责人):负责对备忘录的保存和恢复,无须知道备忘录中的实现细节。
UML 结构
场景上下文
在前文 【Go实现】实践GoF的23种设计模式:命令模式 我们提到,在 简单的分布式应用系统(示例代码工程)中,db 模块用来存储服务注册信息和系统监控数据。其中,服务注册信息拆成了 profiles
和 regions
两个表,在服务发现的业务逻辑中,通常需要同时操作两个表,为了避免两个表数据不一致的问题,db 模块需要提供事务功能:
事务的核心功能之一是,当其中某个语句执行失败时,之前已执行成功的语句能够回滚,前文我们已经介绍如何基于 命令模式 搭建事务框架,下面我们将重点介绍,如何基于备忘录模式实现失败回滚的功能。
代码实现
客户端可以这么使用:
这里并没有完全按照标准的备忘录模式 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 来回滚事务,即恢复原先的记录值。
比如,执行一条插入语句:
那么,写入到 undo log 中对应的回滚语句为:
当执行一条语句失败,需要回滚时,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
更多文章请关注微信公众号:元闰子的邀请
版权声明: 本文为 InfoQ 作者【元闰子】的原创文章。
原文链接:【http://xie.infoq.cn/article/d383e2c53c28d24c274e993c0】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论