设计模式-备忘录模式
备忘录模式
文章目录
- 备忘录模式
- 什么是备忘录模式
- 为什么要用备忘录模式
- 如何使用备忘录模式
- 总结
什么是备忘录模式
在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
在我看来,这个模式的定义主要表达了两部分内容。一部分是,存储副本以便后期恢复。这一部分很好理解。另一部分是,要在不违背封装原则的前提下,进行对象的备份和恢复。
为什么要用备忘录模式
接下来,我就结合一个例子来解释一下,特别带你搞清楚这两个问题:
- 为什么存储和恢复副本会违背封装原则?
- 备忘录模式是如何做到不违背封装原则的?
假设有这样一道面试题,希望你编写一个小程序,可以接收命令行的输入。用户输入文本时,程序将其追加存储在内存文本中;用户输入“:list”,程序在命令行中输出内存文本的内容;用户输入“:undo”,程序会撤销上一次输入的文本,也就是从内存文本中将上次输入的文本删除掉。 我举了个小例子来解释一下这个需求,如下所示:
>hello
>:list
hello
>world
>:list
helloworld
>:undo
>:list
hello
怎么来编程实现呢?你可以打开 IDE 自己先试着编写一下,然后再看我下面的讲解。整体上来讲,这个小程序实现起来并不复杂。我写了一种实现思路,如下所示:
public class InputText {private StringBuilder text = new StringBuilder();public String getText() {return text.toString();}public void append(String input) {text.append(input);}public void setText(String text) {this.text.replace(0, this.text.length(), text);}
}public class SnapshotHolder {private Stack<InputText> snapshots = new Stack<>();public InputText popSnapshot() {return snapshots.pop();}public void pushSnapshot(InputText inputText) {InputText deepClonedInputText = new InputText();deepClonedInputText.setText(inputText.getText());snapshots.push(deepClonedInputText);}
}public class ApplicationMain {public static void main(String[] args) {InputText inputText = new InputText();SnapshotHolder snapshotsHolder = new SnapshotHolder();Scanner scanner = new Scanner(System.in);while (scanner.hasNext()) {String input = scanner.next();if (input.equals(":list")) {System.out.println(inputText.getText());} else if (input.equals(":undo")) {InputText snapshot = snapshotsHolder.popSnapshot();inputText.setText(snapshot.getText());} else {snapshotsHolder.pushSnapshot(inputText);inputText.append(input);}}}
}
实际上,备忘录模式的实现很灵活,也没有很固定的实现方式,在不同的业务需求、不同编程语言下,代码实现可能都不大一样。上面的代码基本上已经实现了最基本的备忘录的功能。但是,如果我们深究一下的话,还有一些问题要解决,那就是前面定义中提到的第二点:要在不违背封装原则的前提下,进行对象的备份和恢复。而上面的代码并不满足这一点,主要体现在下面两方面:
- 第一,为了能用快照恢复 InputText 对象,我们在 InputText 类中定义了 setText() 函数,但这个函数有可能会被其他业务使用,所以,暴露不应该暴露的函数违背了封装原则;
- 第二,快照本身是不可变的,理论上讲,不应该包含任何 set() 等修改内部状态的函数,但在上面的代码实现中,“快照“这个业务模型复用了 InputText 类的定义,而 InputText 类本身有一系列修改内部状态的函数,所以,用 InputText 类来表示快照违背了封装原则。
如何使用备忘录模式
针对以上问题,我们对代码做两点修改。其一,定义一个独立的类(Snapshot 类)来表示快照,而不是复用 InputText 类。这个类只暴露 get() 方法,没有 set() 等任何修改内部状态的方法。其二,在 InputText 类中,我们把 setText() 方法重命名为 restoreSnapshot() 方法,用意更加明确,只用来恢复对象。
按照这个思路,我们对代码进行重构。重构之后的代码如下所示:
public class InputText {private StringBuilder text = new StringBuilder();public String getText() {return text.toString();}public void append(String input) {text.append(input);}public Snapshot createSnapshot() {return new Snapshot(text.toString());}public void restoreSnapshot(Snapshot snapshot) {this.text.replace(0, this.text.length(), snapshot.getText());}
}public class Snapshot {private String text;public Snapshot(String text) {this.text = text;}public String getText() {return this.text;}
}public class SnapshotHolder {private Stack<Snapshot> snapshots = new Stack<>();public Snapshot popSnapshot() {return snapshots.pop();}public void pushSnapshot(Snapshot snapshot) {snapshots.push(snapshot);}
}public class ApplicationMain {public static void main(String[] args) {InputText inputText = new InputText();SnapshotHolder snapshotsHolder = new SnapshotHolder();Scanner scanner = new Scanner(System.in);while (scanner.hasNext()) {String input = scanner.next();if (input.equals(":list")) {System.out.println(inputText.toString());} else if (input.equals(":undo")) {Snapshot snapshot = snapshotsHolder.popSnapshot();inputText.restoreSnapshot(snapshot);} else {snapshotsHolder.pushSnapshot(inputText.createSnapshot());inputText.append(input);}}}
}
总结
前面我们只是简单介绍了备忘录模式的原理和经典实现,现在我们再继续深挖一下。如果要备份的对象数据比较大,备份频率又比较高,那快照占用的内存会比较大,备份和恢复的耗时会比较长。这个问题该如何解决呢?
不同的应用场景下有不同的解决方法。比如,我们前面举的那个例子,应用场景是利用备忘录来实现撤销操作,而且仅仅支持顺序撤销,也就是说,每次操作只能撤销上一次的输入,不能跳过上次输入撤销之前的输入。在具有这样特点的应用场景下,为了节省内存,我们不需要在快照中存储完整的文本,只需要记录少许信息,比如在获取快照当下的文本长度,用这个值结合 InputText 类对象存储的文本来做撤销操作。
我们再举一个例子。假设每当有数据改动,我们都需要生成一个备份,以备之后恢复。如果需要备份的数据很大,这样高频率的备份,不管是对存储(内存或者硬盘)的消耗,还是对时间的消耗,都可能是无法接受的。想要解决这个问题,我们一般会采用“低频率全量备份”和“高频率增量备份”相结合的方法。
全量备份就不用讲了,它跟我们上面的例子类似,就是把所有的数据“拍个快照”保存下来。所谓“增量备份”,指的是记录每次操作或数据变动。
当我们需要恢复到某一时间点的备份的时候,如果这一时间点有做全量备份,我们直接拿来恢复就可以了。如果这一时间点没有对应的全量备份,我们就先找到最近的一次全量备份,然后用它来恢复,之后执行此次全量备份跟这一时间点之间的所有增量备份,也就是对应的操作或者数据变动。这样就能减少全量备份的数量和频率,减少对时间、内存的消耗。
相关文章:
设计模式-备忘录模式
备忘录模式 文章目录 备忘录模式什么是备忘录模式为什么要用备忘录模式如何使用备忘录模式总结 什么是备忘录模式 在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。 在我看来…...

阿里、京东等大厂年薪50w的测试都是什么水平?
各位做测试的朋友,但凡经历过几次面试,那么你一定曾被问到过以下问题: 1、在Linux环境下,怎么执行web自动化测试? 2、Shell如何,Docker熟悉吗? 3、全链路的压测实操过吗,如何推进与开…...

Java PECS(Producer Extends Consumer Super)原则
在看 Alibaba 开发手册时遇到 PECS 原则,刚开始阅读时感觉比较绕,也搜索了一些博文参考,个人觉得 Stackoverflow 的这篇文章比较实用 —— What is PECS (Producer Extends Consumer Super)? 后面结合 JDK 源码梳理了下 // java/util/List…...

Learn RabbitMQ with SpringBoot
文章目录 What is RabbitMQ?RabbitMQ Core conceptRabbitMQ ArchitectureInstall and setup RabbitMQ using DockerExplore RabbitMQ using management UICreate and setup Springboot3 project in intellijSpringboot and RabbitMQ Basic MessageConnection between Springbo…...
定时器 POSIX Timer定时器和setitimer定时器
POSIX 可移植 POSIX:可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX 。 POSIX Timer C API 总结POSIX系统的C标准库: 函数描述clock_settime()通过指定Value设置clock的分辨率clock_gettime()…...

DeSD:用于3D医学图像分割的深度自蒸馏自监督学习
文章目录 DeSD: Self-Supervised Learning with Deep Self-Distillation for 3D Medical Image Segmentation摘要本文方法Deep Self-DistillationDownstream Transfer Learning 实验结果 DeSD: Self-Supervised Learning with Deep Self-Distillation for 3D Medical Image Seg…...
MySQL数据库——MySQL创建触发器(CREATE TRIGGER)
触发器是与 MySQL 数据表有关的数据库对象,在满足定义条件时触发,并执行触发器中定义的语句集合。触发器的这种特性可以协助应用在数据库端确保数据的完整性。 基本语法 在 MySQL 5.7 中,可以使用 CREATE TRIGGER 语句创建触发器。 语法格…...

Java实现网上人才招聘系统【附源码】
网上人才招聘系统 1、概述 3 2、系统分析 4 2.1、问题定义 4 2.2、可行性研究 4 2.2.1、可行性需求分析 4 2.2.2、数据流分析 5 2.2.3、数据字典 6 2.2.4、程序流程图 6 2.2.4、开发进度计划 6 2.3、需求分析 7 2.3.1、功能需求分析 7 2.3.2、数据需求分析 10 2.3.3、性能需求…...

jmeter接口测试项目实战详解,零基础也能学,源码框架都给你
目录 1.什么是jmeter? 2.jmeter能做什么? 3.jmeter环境搭建 3.1前提: 3.2jmeter下载: 3.3jmeter环境搭建: 3.3.1mac当中jmeter环境搭建: 3.4jmeter基本配置 3.4.1.切换语言 3.4.2.安装插件 4.jmet…...

MySQL中去重 distinct 和 group by 是如何去重的
1:测试数据 CREATE TABLE student (stu_no VARCHAR(40) NOT NULL,name VARCHAR(100) NOT NULL );insert into student values(1,name1); insert into student values(2,name2); insert into student values(3,name1); insert into student values(4,name2); i…...

在职读研是理想还是情怀?你想要的都将在社科大能源管理硕士项目实现
在职读研是理想还是情怀呢,每个读研人的想法不同,原因也有所不同。但选择在职继续攻读硕士学位的群体也有着共同点,他们都是想拥有高学历,拥有高目标的一群人。探寻新的起点和终点是他们想所要追求的。不管读研的初心是什么&#…...

携手共建数字钢铁,Hightopo亮相第三届钢铁展洽会
4 月 26 日备受期待的第三届钢铁展洽会在日照盛大召开。图扑软件作为智慧钢铁行业领先的 2D 和 3D 图形界面可视化解决方案提供商,受邀参与此次展会。 图扑软件携智慧钢铁三维可视化监控体系亮相“钢铁展洽会”,向众多钢铁企业展示了一系列图扑 HT 数字…...

Leetcode2383. 赢得比赛需要的最少训练时长
Every day a Leetcode 题目来源:2383. 赢得比赛需要的最少训练时长 解法1:模拟 可以分开考虑在比赛开始前,需要最少增加的精力和经验数量。 每次遇到一个对手,当前精力值都需要严格大于当前对手,否则需要增加精力值…...

js代码执行过程、调用栈、执行上下文
参考资料 极客时间课程《浏览器工作原理与实践》 – 李兵 一、js代码执行过程 (一)javascript代码的执行流程 浏览器执行javascript代码的流程如下图所示: javascript的执行机制是:先编译,再执行。在编译阶段生成了…...
互联网摸鱼日报(2023-05-12)
互联网摸鱼日报(2023-05-12) InfoQ 热门话题 建设和改进持续业务交付能力| BizDevOps 公开课 一部手机就可运行,精通Python等20种语言!谷歌终于能与OpenAI 打擂台了,全新PaLM 2比肩GPT-4 蚂蚁数科开发者…...
【Python从入门到实践3.1】扑克发牌知识点(range函数,def函数,else语句配合使用,random库,列表推导式)
扑克发牌知识点 range函数def函数else语句配合使用:random库列表推导式 本篇博文需要特别感谢"Python从入门到精通"课程中一位同学对扑克发牌程序做出的知识点分析,本博文的内容大多也是从这位同学的分析而来. range函数 Range()函数: *返回一…...

Spring Cloud第二季--Spring Cloud Bus
文章目录 Spring Clud Bus什么是总线基本原理 牛刀小试 Spring Clud Bus 在Spring Cloud学习–配置中心(Config)中实现了集中管理微服务配置、不同环境不同配置、运行期间也可动态调整、配置修改后可以自动更新的需求,但同时也有一个弊端&am…...

Unittest自动化测试之unittestunittest_生成测试报告
unittest_生成测试报告 测试报告为测试结果的统计即展示,是自动化测试不可或缺的一部分,利用unittest 可以生成测试报告 方式一、使用第三方 HTMLTestRunner 执行测试用例集,生成网页版测试报告(推荐) HTMLTestRunn…...
一个查询IP地理信息和CDN提供商的离线终端工具
Nali 功能 支持多种数据库 纯真 IPv4 离线数据库ZX IPv6 离线数据库Geoip2 城市数据库 (可选)IPIP 数据库 (可选)ip2region 数据库 (可选)DB-IP 数据库 (可选)IP2Location DB3 LITE 数据库 (可选) CDN 服务提供商查询支持管道处理支持交互式查询同时支持IPv4和IPv6支持多语言…...

RflySim平台使用篇 | Rflysim3D软件使用系列教程(二)
导读: RflySim3D(支持体验版)和RflySimUE5(支持完整版)为本平台核心三维显示软件, 分别基于UE4 和UE5 引擎开发,具备高逼真虚拟现实显示效果。本视频主要讲解了如何将自定义的三维场景如何加载到RflySim3D…...

铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...

2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...

《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...

Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...

Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...

有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...