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

    浏览量

    30751
  • MySQL
    +关注

    关注

    1

    文章

    775

    浏览量

    26005
  • MVCC
    +关注

    关注

    0

    文章

    13

    浏览量

    1437

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

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

收藏 人收藏

    评论

    相关推荐

    70 面向对向设计模式实践备忘录(快照)模式

    前端
    小凡
    发布于 :2022年08月28日 16:12:28

    高颜值智能备忘录:不再遗忘任何要事

    摸出手机,在屏幕上找出备忘录,打字,纠错,排版...感觉好心塞...或者某天你在跑步,突然想起要给明天生日的女朋友买礼物,你在心里反复提醒,然后...跪了一晚上榴莲...面对这些情况,小编特别想给大家
    发表于 05-19 21:48

    PostgreSQL操作备忘录

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

    UDS诊断命令备忘录

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

    MSP430单片机C语言基础知识大合集

    前言《MSP430单片机应用基础与实践》(华中科技大学出版社)------第1章------计算机的基础知识(本文章作备忘录使用)
    发表于 11-29 07:25

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

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

    keil5MDK和eplan2.7安装备忘录相关资料分享

    备忘录是防止以后安装的时候忘记步骤和主要问题。keil5安装我第一次使用的是软件安装管家的安装包,安完以后发现注册码一直弄不上,芯片库里边也没有STM32的芯片。然后找到大二时候电子工艺实习
    发表于 01-10 07:06

    23基本的设计模式总结

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

    23种java设计模式

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

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

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

    设计模式备忘录设计模式

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

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

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

    实践GoF23种设计模式:适配器模式

    适配器模式所做的就是将一个接口 Adaptee,通过适配器 Adapter 转换成 Client 所期望的另一个接口 Target 来使用,实现原理也很简单,就是 Adapter 通过实现 Target接口,并在对应的方法中调用 Adaptee 的接口实现。
    的头像 发表于 12-10 14:00 291次阅读
    <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>

    亿纬锂能与Aksa签署谅解备忘录,共建土耳其合资公司

    根据这份谅解备忘录, 亿纬锂能和Aksa同意在土耳其设立一个合资企业。至于合资企业的股权结构、管理模式、融资途径等具体内容将在未来的合资协议中予以详述。
    的头像 发表于 01-16 10:22 295次阅读

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

    解释器模式(Interpreter Pattern)应该是 GoF23 种设计模式中使用频率最少的一种了,它的应用场景较为局限。
    的头像 发表于 04-01 11:01 168次阅读
    <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>