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

news2025/2/22 6:02:23

上一篇:【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 模块用来存储服务注册信息和系统监控数据。其中,服务注册信息拆成了 profilesregions 两个表,在服务发现的业务逻辑中,通常需要同时操作两个表,为了避免两个表数据不一致的问题,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 进行实现,但本质是一样的,总结起来有以下几个关键点:

  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画出手绘风格的配图 中找到文章的绘图方法。

参考

[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

更多文章请关注微信公众号:元闰子的邀请

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1253817.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

红队攻防实战系列一之metasploit

百目无她,百书质华,君当醒悟,建我中华 本文首发于先知社区,原创作者即是本人 前言 在红队攻防中,我们主要在外网进行信息收集,通过cms或者其他漏洞拿到shell,之后通过免杀木马将windows或lin…

【通讯协议】gRPC和Webhook

RPC(Remote procedure Call)之所以被称为“远程”,是因为在微服务架构下,当服务部署到不同的服务器上时,它可以实现远程服务之间的通信。从用户的角度来看,它的作用就像本地函数调用。 下图说明了gRPC的整…

java io 流,输入流和输出流;节点流和处理流;字节流和字符流

文章目录 java 中 IO 流分为几种?按照流的流向分,可以分为输入流和输出流;按照流的角色划分为节点流和处理流。IO流主要的分类方式有以下3种: java中的IO流也是工作中使用到比较频繁的一个内容,今天以这篇文章来了解它的概念和整…

第十七章 解读PyTorch断点训练(工具)

主要有以下几方面的内容: 对于多步长训练需要保存lr_schedule初始化随机数种子保存每一代最好的结果 简单详细介绍 最近在尝试用CIFAR10训练分类问题的时候,由于数据集体量比较大,训练的过程中时间比较长,有时候想给停下来&…

下一代图片压缩格式 AVIF

长期以来我们都在为了在网络上使用什么样的图片格式而进行纠结。我们所熟知的或者运用到 Web 应用中的图片格式无非就是 PNG、JPG、GIF、SVG 或者 WebP。 HEIC是一种图像格式,上线时间还比较短,只有4年左右。 自iOS 11和 macOS High Sierra&#xff08…

基于 Gin 的 HTTPS 代理 Demo

上次写了 基于 Gin 的 HTTP 代理 Demo 之后,对这方面还是蛮感兴趣的,所以就接着继续走下去。为了这个主题的内容,我斥巨资购入了一本二手的 《HTTP 权威指南》,因为我知道这本书里面有我想要的知识。在我还在大学的时候&#xff0…

Kerberos 高可用配置和验证

参考 https://cloud.tencent.com/developer/article/1078314 https://mp.weixin.qq.com/s?__bizMzI4OTY3MTUyNg&mid2247485861&idx1&snbb930a497f63ac5e63ed20c64643eec5 机器准备 Kerberos主 ip-172-31-22-86.ap-southeast-1.compute.internal 7.common2.hado…

美国季节性干旱数据集

美国季节性干旱数据集 美国干旱展望栅格数据集由国家气象局气候预测中心生成。它在每个月的最后一天发布,提供下个月的干旱前景信息。“美国季节性干旱展望”数据集每月发布一次,特别是每月的第三个星期四。该数据集对美国不同地区发生干旱的可能性进行…

Linux加强篇005-用户身份与文件权限

目录 前言 1. 用户身份与能力 2. 文件权限与归属 3. 文件的特殊权限 4. 文件的隐藏属性 5. 文件访问控制列表 6. su命令与sudo服务 前言 悟已往之不谏,知来者之可追。实迷途其未远,觉今是而昨非。舟遥遥以轻飏,风飘飘而吹衣。问征夫以…

AIGC创作系统ChatGPT网站源码、支持最新GPT-4-Turbo模型、GPT-4图片对话能力+搭建部署教程

一、AI创作系统 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统,支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美,可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI…

居家适老化设计第三十一条---卫生间水龙头

以上产品图片均来源于淘宝 侵权联系删除 居家适老化中,水龙头是一个非常重要的设备。水龙头的选择应该考虑到老年人的特点和需求。首先,水龙头的操作应该简单方便,老年人手部灵活性可能不如年轻人,因此水龙头应该设计成易于转动和…

计算机网络常考计算题之循环冗余校验(宝典教学)

文章目录 奇偶效验循环冗余校验例题四步走另一种题型 本文讲述了计算机考研中易出现的循环冗余校验,点赞关注收藏不迷路哦 我是一名双非计算机本科生,希望我的文章可以帮助到你。 奇偶效验 奇偶校验:也可以检测数据在传输过程中是否出现错误…

Ceph----RBD块存储的使用:详细实践过程实战版

RBD 方式的 工作 流程: 1、客户端创建一个pool,并指定pg数量,创建 rbd 设备并map 到文件系统; 2、用户写入数据,ceph进行对数据切块,每个块的大小默认为 4M,每个 块名字是 object序号&#xff…

【UnLua】在 Lua 中定义 UE 反射类型

【UnLua】在 Lua 中定义 UE 反射类型 用法 启动编辑器时遍历 Defines 目录下 lua 脚本来加载 UE 反射类型(开个临时的 Lua VM 即可)直接像 -- define a uenum in lua UEnum.EEnumGuestSomethingElse {Value1 1;Value2 2; }-- use it like a native …

算法基础之单链表

单链表 核心思想&#xff1a; 用数组模拟链表(new节点非常慢 用数组模拟快) e[N] 表示节点value ne[N]表示next指针指向 (空节点为-1) #include<iostream>using namespace std;const int N100010;//head头结点的指针//e[N] 表示节点value ne[N]表示next指针指向 //idx…

聚簇索引和非聚簇索引的区别;什么是回表

聚簇索引和非聚簇索引的区别 什么是聚簇索引&#xff1f;&#xff08;重点&#xff09; 聚簇索引就是将数据(一行一行的数据)跟索引结构放到一块&#xff0c;InnoDB存储引擎使用的就是聚簇索引&#xff1b; 注意点&#xff1a; 1、InnoDB使用的是聚簇索引&#xff08;聚簇索…

学习.NET验证模块FluentValidation的基本用法(续2:其它常见用法)

FluentValidation模块支持调用When和Unless函数设置验证规则的执行条件&#xff0c;其中when函数设置的是满足条件时执行&#xff0c;而Unless函数则是满足条件时不执行&#xff0c;这两个函数的使用示例如及效果如下所示&#xff1a; public AppInfoalidator() {RuleFor(x>…

CodeTON Round 7(D、E)

D - Ones and Twos 题意&#xff1a;给你一个长度为 n 的 数组 a &#xff0c;其中每个元素都是 1 或 2。请处理以下两种类型的询问 "1 s"&#xff1a;检查是否存在 a的子数组 &#xff0c;其总和等于 s。 "2 i v"&#xff1a;将 ai改为v。 如果数组 …

MySQL性能优化,SQL调优,SQL调优的手段

文章目录 对MySQL性能的优化的理解硬件和操作系统层面的优化架构设计层面的优化MySQL程序配置优化SQL优化 SQL调优有哪几种方式1.EXPLAIN2.SQL语句中IN包含的值不应过多3.SELECT语句务必指明字段名称4.当只需要一条数据的时候&#xff0c;使用limit 15.如果排序字段没有用到索引…

(附源码)SSM环卫人员管理平台 计算机毕设36412

目 录 摘要 1 绪论 1.1背景及意义 1.2国内外研究概况 1.3研究内容 1.4 ssm框架介绍 1.5论文结构与章节安排 2 环卫人员管理平台系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1数据增加流程 2.2.2数据修改流程 2.2.3数据删除流程 2.3 系统功能分析 2.3.1 功能性…