0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

实践GoF的23种设计模式:备忘录模式

元闰子的邀请 来源:元闰子的邀请 2023-11-25 09:05 次阅读

简介

相对于代理模式、工厂模式等设计模式,备忘录模式(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 结构

1d4190e0-8b2d-11ee-939d-92fbcf53809c.png

场景上下文

在前文【Go实现】实践GoF的23种设计模式:命令模式我们提到,在简单的分布式应用系统(示例代码工程)中,db 模块用来存储服务注册信息和系统监控数据。其中,服务注册信息拆成了profiles和regions两个表,在服务发现的业务逻辑中,通常需要同时操作两个表,为了避免两个表数据不一致的问题,db 模块需要提供事务功能:

1d75d79c-8b2d-11ee-939d-92fbcf53809c.png

事务的核心功能之一是,当其中某个语句执行失败时,之前已执行成功的语句能够回滚,前文我们已经介绍如何基于命令模式搭建事务框架,下面我们将重点介绍,如何基于备忘录模式实现失败回滚的功能。

代码实现

//demo/db/transaction.go
packagedb

//Command执行数据库操作的命令接口,同时也是备忘录接口
//关键点1:定义Memento接口,其中Exec方法相当于UML图中的SetState方法,调用后会将状态保存至Db中
typeCommandinterface{
Exec()error//Exec执行insert、update、delete命令
Undo()//Undo回滚命令
setDb(dbDb)//SetDb设置关联的数据库
}

//关键点2:定义Originator,在本例子中,状态都是存储在Db对象中
typeDbinterface{...}

//TransactionDb事务实现,事务接口的调用顺序为begin->exec->exec>...->commit
//关键点3:定义Caretaker,Transaction里实现了对语句的执行(Do)和回滚(Undo)操作
typeTransactionstruct{
namestring
//关键点4:在Caretaker(Transaction)中引用Originator(Db)对象,用于后续对其状态的保存和恢复
dbDb
//注意,这里的cmds并非备忘录列表,真正的history在Commit方法中
cmds[]Command
}
//Begin开启一个事务
func(t*Transaction)Begin(){
t.cmds=make([]Command,0)
}
//Exec在事务中执行命令,先缓存到cmds队列中,等commit时再执行
func(t*Transaction)Exec(cmdCommand)error{
ift.cmds==nil{
returnErrTransactionNotBegin
}
cmd.setDb(t.db)
t.cmds=append(t.cmds,cmd)
returnnil
}
//Commit提交事务,执行队列中的命令,如果有命令失败,则回滚后返回错误
func(t*Transaction)Commit()error{
//关键点5:定义备忘录列表,用于保存某一时刻的系统状态
history:=&cmdHistory{history:make([]Command,0,len(t.cmds))}
for_,cmd:=ranget.cmds{
//关键点6:执行Do方法
iferr:=cmd.Exec();err!=nil{
//关键点8:当Do方法执行失败时,则进行Undo操作,根据备忘录history中的状态进行回滚
history.rollback()
returnerr
}
//关键点7:如果Do方法执行成功,则将状态(cmd)保存在备忘录history中
history.add(cmd)
}
returnnil
}
//cmdHistory命令执行历史
typecmdHistorystruct{
history[]Command
}
func(c*cmdHistory)add(cmdCommand){
c.history=append(c.history,cmd)
}

func(c*cmdHistory)rollback(){
fori:=len(c.history)-1;i>=0;i--{
c.history[i].Undo()
}
}

//InsertCmd插入命令
//关键点9:定义具体的备忘录类,实现Memento接口
typeInsertCmdstruct{
dbDb
tableNamestring
primaryKeyinterface{}
newRecordinterface{}
}

func(i*InsertCmd)Exec()error{
returni.db.Insert(i.tableName,i.primaryKey,i.newRecord)
}
func(i*InsertCmd)Undo(){
i.db.Delete(i.tableName,i.primaryKey)
}
func(i*InsertCmd)setDb(dbDb){
i.db=db
}

//UpdateCmd更新命令
typeUpdateCmdstruct{...}
//DeleteCmd删除命令
typeDeleteCmdstruct{...}

客户端可以这么使用:

funcclient(){
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)
iferr:=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 来回滚事务,即恢复原先的记录值。

比如,执行一条插入语句:

insertintoregion(id,name)values(1,"beijing");

那么,写入到 undo log 中对应的回滚语句为:

deletefromregionwhereid=1;

当执行一条语句失败,需要回滚时,MySQL 就会从读取对应的回滚语句来执行,从而将数据恢复至事务提交之前的状态。undo log 是 MySQL 实现事务回滚和多版本控制(MVCC)的根基。

典型应用场景

事务回滚。事务回滚的一种常见实现方法是 undo log,其本质上用的就是备忘录模式。

系统快照(Snapshot)。多版本控制的用法,保存某一时刻的系统状态快照,以便在将来能够恢复。

撤销功能。比如 Microsoft Offices 这类的文档编辑软件的撤销功能。

优缺点

优点

提供了一种状态恢复的机制,让系统能够方便地回到某个特定状态下。

实现了对状态的封装,能够在不破坏封装的前提下实现状态的保存和恢复。

缺点

资源消耗大。系统状态的保存意味着存储空间的消耗,本质上是空间换时间的策略。undo log 是一种折中方案,保存的状态并非某一时刻数据库的所有数据,而是一条反操作的 SQL 语句,存储空间大大减少。

并发安全。在多线程场景,实现备忘录模式时,要注意在保证状态的不变性,否则可能会有并发安全问题。

与其他模式的关联

在实现 Undo/Redo 操作时,你通常需要同时使用备忘录模式与命令模式。

另外,当你需要遍历备忘录对象中的成员时,通常会使用迭代器模式,以防破坏对象的封装。

文章配图

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






审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • UML
    UML
    +关注

    关注

    0

    文章

    122

    浏览量

    30858
  • MySQL
    +关注

    关注

    1

    文章

    804

    浏览量

    26530
  • MVCC
    +关注

    关注

    0

    文章

    13

    浏览量

    1465

原文标题:【Go实现】实践GoF的23种设计模式:备忘录模式

文章出处:【微信号:yuanrunzi,微信公众号:元闰子的邀请】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    HarmonyOS开发实例:【手机备忘录

    基于用户首选项,实现了备忘录新增、更新、删除以及查找等功能。
    的头像 发表于 04-18 21:40 791次阅读
    HarmonyOS开发实例:【手机<b class='flag-5'>备忘录</b>】

    PostgreSQL操作备忘录

    PostgreSQL 操作备忘录
    发表于 05-23 08:48

    UDS诊断命令备忘录

    UDS实践性强,逻辑复杂,很多服务非要体验过一次才能理解,导致包括我在内的初学者感觉晦涩难懂,不明觉厉,因此将自己的理解写下来、整理下来,与君共勉。零、UDS诊断命令备忘录一、简介UDS
    发表于 08-26 16:09

    怎样去搭建一基于XR806的开源桌面备忘录

    本人计划怼一个开源桌面备忘录/天气预报/相册的项目基于XR806,同时学习鸿蒙操作系统获得晕哥赠送的开发板和芯片,目前处于环境搭建阶段看起来这个芯片玩的人比较少,目前遇到了问题,不知道如何解决,希望
    发表于 12-28 06:52

    23基本的设计模式总结

    一样。​提到设计模式,不得不感谢GoF(***,四人组),他们1995年出版的《设计模式》一书,第一次将设计模式提升到理论高度,并将之规范化。书中一共总结了
    发表于 03-01 06:08

    全球半导体联盟与中国半导体行业签署合作备忘录

    全球半导体联盟与中国半导体行业签署合作备忘录 全球半导体联盟(GSA)与中国半导体行业协会(CSIA)在苏州联合申明签署合作备忘录。此次合作将为促
    发表于 09-24 08:17 698次阅读

    戴姆勒与百度签署谅解备忘录

    7月25日,奔驰母公司戴姆勒与百度签署谅解备忘录,深化双方在自动驾驶和车联网等领域的战略合作。
    的头像 发表于 07-28 09:53 2719次阅读

    23java设计模式

    JAVA的设计模式经前人总结可以分为23 设计模式根据使用类型可以分为三: 1、创建模式
    发表于 09-23 15:17 1次下载

    实践GoF23设计模式:命令模式简介

    因此,我们需要对请求进行抽象,将上下文信息封装到请求对象里,这其实就是命令模式,而该请求对象就是 Command。
    的头像 发表于 01-13 16:36 761次阅读

    设计模式备忘录设计模式

    备忘录设计模式(Memento Design Pattern)是一行为型设计模式,它的主要目的是在不破坏对象封装性的前提下,捕捉和保存一个对象的内部状态
    的头像 发表于 06-06 11:19 798次阅读

    设计模式行为型:备忘录模式

    备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式
    的头像 发表于 06-07 11:16 852次阅读
    设计<b class='flag-5'>模式</b>行为型:<b class='flag-5'>备忘录</b><b class='flag-5'>模式</b>

    新思科技同越南政府签署谅解备忘录

    在越南总理范明政访美期间,新思科技与越南国家创新中心(nic)签署了关于培养越南集成电路设计人才的谅解备忘录,支持nic成立芯片设计孵化中心。另外,新思科技与越南信息通讯部下属的信息通信技术产业公司签订了支援越南半导体产业发展的谅解备忘录
    的头像 发表于 09-20 10:56 1552次阅读

    实践GoF23设计模式:解释器模式

    解释器模式(Interpreter Pattern)应该是 GoF23 设计模式中使用频率最少的一
    的头像 发表于 04-01 11:01 689次阅读
    <b class='flag-5'>实践</b><b class='flag-5'>GoF</b>的<b class='flag-5'>23</b><b class='flag-5'>种</b>设计<b class='flag-5'>模式</b>:解释器<b class='flag-5'>模式</b>

    苹果iOS 18将支持语音备忘录及数学符号显示

    首先是语音备忘录功能。据悉,苹果有意在iOS 18系统中加入此项功能,使iPhone用户能够便捷地录制音频文件,并将其直接嵌入至备忘录之中。
    的头像 发表于 04-18 11:14 519次阅读

    亿纬锂能与马来西亚拉曼理工大学签署合作备忘录

    备忘录的签署,旨在通过产学联合,搭建教育与产业融合的发展框架,推动实践性人才培养,促进双方资源共享、优势互补。
    的头像 发表于 10-24 10:30 223次阅读