1、前言

本模式用的不是特别多,知道即可,本文主要是平时的读书笔记的整理

2、出现的动机和概念

备忘录模式——也叫 Memo 模式,或者快照模式等,顾名思义就是实现历史记录的作用,比如可以实现游戏关卡的角色复活,任务进度保存,命令的撤销,以及系统的快照留存记录等功能。

备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉(Capture),并外部化存储,从而可以在将来合适的时候把这个对象还原到存储时的状态(undo/rollback)。

很简单的概念,可以联系Git,还有数据库事务处理等,它们都有版本记录,操作回滚的逻辑,这些都可以基于备忘录模式,搭配其他模式来优雅的实现。

一句话:当业务需求是让对象返回之前的某个历史性的状态的时候,就应该使用备忘录模式加以封装。

2.1、什么叫破坏了封装性

假如保存某个对象 A 的当前状态 A1,那么 RD 自然不是撑得没事干,肯定是为了未来能回滚或者查看对象 A 的这个当前状态 A1。自然的,外部的类(对象)就一定要能够自由的访问 A 的内部状态(即有一段代码 B 需要依赖 A 的内部结构),否则连保存什么都不知道,那还保存个什么劲儿呢。那么问题来了,如果稍不注意,就会把 B 分散在系统的各个角落,导致系统对 A 恢复操作的管理日益杂乱,增大开发和维护成本。这就叫破坏了封装性。

2.2、如何防止关键对象的封装性遭到破坏

答案很明显,就是使用备忘录模式加以设计。

3、由投色子游戏引出

游戏会有玩家复活功能,或者关卡进度恢复的功能,如果你不提供这样的功能,肯定没人玩。但是游戏的状态是非常关键的数据,必须要封装得当,不能让别人随意访问。

下面看一个投色子的游戏机例子,玩家先充钱(200起步)才能玩,且按下投掷按钮,让机器来摇色子:

1、点数为1,玩家赢100块钱

2、为2,输200块钱

3、为6,玩家不赢钱,但是可以得到一个礼物,礼物里分为两类:

  • 纪念意义的礼物,不值钱

  • 可以积累换积分,兑换钱的vip礼物

4、玩家没钱了,游戏结束

5、如果玩家不想结束当前游戏,则可以充钱恢复到最初状态。

我们用 User 代表玩家,Memo 代表备忘录,Game 代表游戏机。

3.1、备忘录类和单一职责原则

Memo 代表备忘录类,是备忘录模式的核心类。顾名思义,它只有一个功能——负责保存和恢复目标对象的状态,比如创建快照,恢复快照。而到底什么时候创建快照,什么时候恢复快照,Memo 类并不关心。

在例子中,Memo 表示玩家的状态,注意该类和代表玩家类的类(User)都必须在一个包下面。

import java.util.ArrayList;
import java.util.List;/*** 特别要注意,各个属性和方法的 包 权限,它和用户类需在一个包下*/
public class Memo {/*** 代表用户的钱,为包访问权限*/int money;/*** 代表用户的礼物,为包访问权限*/ArrayList gifts;/*** 包访问权限的构造器——这是一个宽接口*/Memo(int money) {this.money = money;this.gifts = new ArrayList<>();}// 窄接口,获取用户的当前状态下的钱public int getMoney() {return money;}/*** 宽接口,保存礼物*/void addGift(String gift) {this.gifts.add(gift);}/*** 宽接口,获取当前用户持有的所有礼物*/List getGifts() {return (List) this.gifts.clone();}
}

一定注意 Memo 类的成员权限,这非常重要:

1、构造器在包外无法被访问,只有本包内的类可以访问,生成 Memo 实例

2、addGift 方法也是只有同一个包下的类能访问——给用户保存所得的礼物,外部包的类无法改变 Memo(备忘录)的数据

3、只有 getMoney 是 public,虽然只有它能被外界随意访问,但叫窄接口

3.1.1、Java 类成员的访问权限

权限

访问限制的说明

public

任何类

protected

同一个包的类,或者该类的子类

无,也叫默认权限,或者包权限

同一个包的类

private

该类自己

3.1.2、宽接口和窄接口

所谓的宽,窄,要明白针对谁说的——它们都是面向的备忘录 Memo,即宽接口是说其他类调用了该方法,那么就能获得 or 修改 Memo 的所有快照中的数据,这就是所谓的宽的意思。而窄接口,是说其他类调用了该方法,那么只能获得当前快照中的数据,这就是所谓的窄。

在 Memo 类中,只有 public int getMoney() 方法是窄接口,只有它可以被外部的类访问,而修改状态的宽接口们,不可以被外部的类访问。

3.1.3、Java 拷贝

Memo 类里,对 List getGifts 方法的返回值进行了 clone,其中,ArrayList 默认给重写了 clone 方法,但是是浅拷贝的,需要注意。

/*** Returns a shallow copy of this <tt>ArrayList</tt> instance.  (The* elements themselves are not copied.)** @return a clone of this <tt>ArrayList</tt> instance*/
public Object clone() {try {ArrayList<?> v = (ArrayList<?>) super.clone();v.elementData = Arrays.copyOf(elementData, size);v.modCount = 0;return v;} catch (CloneNotSupportedException e) {// this shouldn't happen, since we are Cloneablethrow new InternalError(e);}
}

3.2、和 Memo 类同包的用户类 User(生成者类)

User类——需要被保存状态以便恢复的那个对象。而如何恢复和保存快照的逻辑,它不 care,是前面的 Memo 类负责。

简单的规则就是,只要玩家没有输光了钱,它就可以一直玩下去

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

public class User {private static final String[] GIFT_NAME = {"手机", "扫地机器人", "圆珠笔"};private Random random = new Random();private int money; // 玩家的钱private List gifts = new ArrayList<>();public User(int money) {this.money = money;}public int getMoney() {return money;}
public void playGame() {int dice = random.nextInt(6) + 1; // [1-6]switch (dice) {case 1:this.money += 100;System.out.println("money + 100");break;case 2:this.money -= 200;System.out.println("money -100");break;case 6:String gift = getGift();System.out.println("gitf is " + gift);break;default:System.out.println("平局");}}// 保存快照public Memo captureState() {// 保存当前余额Memo memo = new Memo(this.money);Iterator iterator = this.gifts.iterator(); // 保存当前全部礼物(只保存 VIP 礼物)while (iterator.hasNext()) {String g = (String) iterator.next();if (g.startsWith("VIP")) {memo.addGift(g);}}return memo;}// 恢复快照public void restoreState(Memo memo) {this.money = memo.getMoney();this.gifts = memo.getGifts();}// 模拟随机生成一个礼物给用户,该方法不应该放这里的,为了演示private String getGift() {String prefix = "";if (random.nextBoolean()) {prefix = "VIP: ";}return prefix + GIFT_NAME[random.nextInt(GIFT_NAME.length)];}@Overridepublic String toString() {return "[ money = " + this.money + ", gifts = " + this.gifts + " ]";}
}

captureState 方法用来保存玩家的当前状态(拍摄快照),并把快照返回给调用者,这个调用者就是接下来要实现的管理类。类比拍照,captureState 方法拍下当前玩家的快照,并保存到 Memo 类中。

restoreState 相反就是撤销(回滚,恢复)的操作。

3.3、包外的管理者类 Main——何时保存/恢复快照

Main 类会初始化一个 User 实例,代表一个玩家,在玩家游戏的过程中,由 Main 类决定何时保存 User 快照,何时恢复 User 快照。具体的保存和恢复策略以及存储的位置,是 Memo 这个备忘录类实现的。

如果玩家运气好,会赢钱(礼物),并保存当前快照以便于未来恢复到这个状态。如果运气不好,输光了,玩家会马上买筹码,此时系统自动调用恢复快照的方法,让玩家恢复到死亡之前的状态。

import com.dashuai.D10Memo.memo.Memo;
import com.dashuai.D10Memo.memo.User;

public class Main {public static void main(String[] args) {// 玩家开始游戏,初始化一个用户实例,代表该玩家User user = new User(100);System.out.println("玩家111,买了100筹码,开始游戏");// 保存玩家初始化的状态,这是最早的恢复点Memo memo = user.captureState();for (int i = 1; i <= 10; i++) {System.out.println("用户的当前状态:" + user);System.out.println("------第 " + i + " 局");user.playGame();System.out.println("该局结束后,当前用户的金钱 = " + user.getMoney());if (user.getMoney() > memo.getMoney()) {System.out.println("赢了很多啊,值得保存一下游戏进度");memo = user.captureState();System.out.println("保存完毕!");} else if (user.getMoney() <= 0) {System.out.println("输光了,复活时间内,用户马上买筹码,为其复活,恢复到游戏结束前的状态");user.restoreState(memo);}}}
}玩家111,买了100筹码,开始游戏
用户的当前状态:[ money = 100, gifts = [] ]
------第 1 局
平局
该局结束后,当前用户的金钱 = 100
用户的当前状态:[ money = 100, gifts = [] ]
------第 2 局
平局
该局结束后,当前用户的金钱 = 100
用户的当前状态:[ money = 100, gifts = [] ]
------第 3 局
平局
该局结束后,当前用户的金钱 = 100
用户的当前状态:[ money = 100, gifts = [] ]
------第 4 局
平局
该局结束后,当前用户的金钱 = 100
用户的当前状态:[ money = 100, gifts = [] ]
------第 5 局
money + 100
该局结束后,当前用户的金钱 = 200
赢了很多啊,值得保存一下游戏进度
保存完毕!
用户的当前状态:[ money = 200, gifts = [] ]
------第 6 局
平局
该局结束后,当前用户的金钱 = 200
用户的当前状态:[ money = 200, gifts = [] ]
------第 7 局
money -200
该局结束后,当前用户的金钱 = 0
输光了,复活时间内,用户马上买筹码,为其复活,恢复到游戏结束前的状态
用户的当前状态:[ money = 200, gifts = [] ]
------第 8 局
gitf is VIP: 圆珠笔
该局结束后,当前用户的金钱 = 200
用户的当前状态:[ money = 200, gifts = [] ]
------第 9 局
平局
该局结束后,当前用户的金钱 = 200
用户的当前状态:[ money = 200, gifts = [] ]
------第 10 局
gitf is VIP: 手机
该局结束后,当前用户的金钱 = 200

1、Main 作为包外的类,就是所谓的管理者类,它管理 User(生成者类) 和 Memo (备忘录类),前者用来表示要保存的对象,后者表示如何保存的逻辑和保存的地点。

2、由于管理者类Main在包外,故 Main 不能直接访问 Memo 类的构造器,无法直接生成备忘录,保证了备忘录的封装完整,Main 只能通过调用 User 类的 public 的 getMoney 方法获取当前玩家的金钱,不能随意改变玩家的余额,保证了安全性。

3、由管理者类——Main 决定,何时拍摄玩家的快照或者何时恢复这个快照。具体的拍照和恢复策略,是 Memo——备忘录类本身实现。

4、备忘录模式的标准类图和角色

1、生成者——对应了示例的 User 类,是需要被保存状态以便恢复的那个对象

2、备忘录——Memo 类,该对象由生成者创建,主要用来保存生成者的内部状态

3、管理者——Main 类,负责管理在适当的时间保存/恢复生成者对象的状态。

备忘录角色有如下责任:

1)将生成者对象的内战状态存储。备忘录可以根据生成者对象的状态判断来决定存储多少生成者对象的内部状态

2)备忘录可以保护其内容不被生成者对象之外的任何对象所读取

5、备忘录模式的应用场景

如果一个对象需要保存状态并可通过undo或rollback等操作恢复到以前的状态时,可以使用Memento模式

具体说,如果一个类需要保存它的对象的状态(相当于生成者角色),可以

1、设计一个类,该类只是用来保存上述对象的状态(相当于 Memo 角色)

2、需要的时候,管理者角色要求生成者返回一个Memo并加以保存

3、undo或rollback时,通过管理者保存的Memo对象,恢复生成者的状态

6、多个备忘录的情景

之前的例子,Main 这个管理者类只保存了一个 memo,如果在Main集成数组或者list,则可以实现历史访问点的快照,便于恢复各个时间点的状态。

7、Memo 备忘录类的有效期问题

如果在内存中保存 memo,那么程序结束,就没用了。此时可把 memo 保存到数据库或者文件里序列化。但是到底保存多久又是个新问题,需要结合具体业务涉及。

8、划分管理者和生成者角色的意义

为什么要这么麻烦呢,直接全部实现在备忘录类不得了么。

因为,管理者角色的职责是决定何时保存生成者的快照,何时撤销。另一方面,生成者角色的职责是代表被保存和恢复的那个对象,他生成备忘录角色对象和使用接受到的备忘录角色对象恢复自己的状态。

这样就实现了职责分离。如下当需求变动:

1、撤销一次的操作,变更为撤销多次时

2、变更拍摄快照保存到内存为保存到数据库,or 文件时

都不需要反复修改生成者角色的代码了,这个生成者是实现关键业务逻辑的类,保证封装的稳定性。

9、经常和备忘录模式搭配的其他模式

备忘录模式常常与命令模式和迭代模式一同使用。比如命令模式实现撤销操作,可以搭配备忘录模式

10、备忘录模式的优缺点

1、优点

把被存储的状态放在外面——Memo角色,不和关键对象(生成者角色)混在一起,维护了各自的内聚和封装性。

能提供快照和恢复功能

2、缺点

如果连接数据库或者文件,可能拍摄快照和恢复的动作比较耗时

11、序列化和备忘录模式

Java中,能使用序列化机制实现对象的状态保存,因此可以搭配序列化机制实现备忘录模式,参看:Java对象序列化全面总结

转载于:https://www.cnblogs.com/kubixuesheng/p/10353325.html

保存快照和撤销功能的实现方案——备忘录模式总结相关推荐

  1. 备忘录模式 Memento 快照模式 标记Token模式 行为型 设计模式(二十二)

    备忘录模式 Memento 沿着脚印,走过你来时的路,回到原点. 苦海翻起爱恨 在世间难逃避命运 相亲竟不可接近 或我应该相信是缘份 一首<一生所爱>触动了多少人的心弦,一段五百年都没有结 ...

  2. phpstduy8 redisClient 2.0 点不了_LOL转区系统2.0试运行公告:提供了贴心的转区快照预览功能...

    亲爱的召唤师: 转区系统将于7月22日下午14:00时重新开放,与旧版转区系统相比,转区系统2.0进行了如下升级与变更: 1.我们不会因为转区冻结召唤师在任何大区的角色,只是转区过程中会短暂临时冻结所 ...

  3. 热像仪 二次开发 c++_重庆多功能红外线热像仪方案

    重庆多功能红外线热像仪方案 yuve4uj 重庆多功能红外线热像仪方案 同时,中文人机对话对识别与合成.自然语言理解.对话管理以及自然语言生成等研究起到的推动作用.月日,我们开始建立个智能方舱,目的是 ...

  4. java设置首页跳转_SpringBoot设置首页(默认页)跳转功能的实现方案

    先给大家介绍下SpringBoot设置首页(默认页)跳转功能 最近springboot开发需要设置个默认页面,就相当于我访问http://www.back.order.baidu.com要直接跳转到登 ...

  5. Android 实现仿微信朋友圈九宫格图片+NineGridView+ImageWatcher(图片查看:1.预览,2.拖动,3.放大,4.左右滑动,5.长按保存到手机)的功能

    一.测试 实现: 二.添加依赖包: implementation 'androidx.appcompat:appcompat:1.1.0'implementation 'androidx.recycl ...

  6. 使用 x32dbg 分析 PC 上 QQ 撤销功能(好友消息和群消息)

    标题:使用 x32dbg 分析 PC 上 QQ 撤销功能(好友消息和群消息) 作者:猫猫.有点乖 文章目录 0x00 前述 0x01 收集信息 0x02 初窥门径 0x03 略有小成 0x04 驾轻就 ...

  7. 如何使用Speccy保存快照

    在Speccy中"快照"是一系列有关你的计算机系统的信息.Speccy快照就类似拍一张照片,你可以在Speccy中拍你的PC的快照并保存,然后再次检查,即使在另一台PC上运行Spe ...

  8. 恒流、限流功能驱动 WLED 方案

    产品简介 AP5414 是一种输入电压范围宽(0.8~5.5V),可调恒定电流和限定电流两种模式来 驱动白光 LED 而设计的升压型 DC/DC 变换器.该器件能利用单节或双节干电池驱动单 颗大功率白 ...

  9. word Ctrl Z 撤销功能无法使用怎么办?

    欢迎技术交流和帮助,提供IT相关服务,索要源码请联系博主QQ: 21497936,若该文为原创文章,未经允许不得转载 原博主博客地址:http://blog.csdn.net/qq21497936 本 ...

最新文章

  1. [转]symbian基本类型转换
  2. Kosaraju 算法查找强连通分支
  3. c加加语言编译程序的首要工作是,选择题(综合答案)
  4. php临时目录没有文件夹里,PHP上传 找不到临时文件夹的解决方法
  5. [html] 怎样在页面上实现一个圆形的可点击区域?
  6. vue解决v-for报错 [vue/valid-v-for]Custom elements in iteration require ‘v-bind:key‘ directives
  7. 【5】dockerfile制作镜像
  8. 通过拦截器获取控制类requestMapping注解中的属性值
  9. python在哪里写代码-程序员面试被要求手写代码,你与顶级程序员的差别在哪?...
  10. 用Keil-MDK开发TQ2440裸机程序入门教程
  11. python前端工资_前端的工资分布情况-你又拖后退了吗?
  12. 微信js-sdk+JAVA实现分享接口
  13. mysql卸载安装pxc_PXC安装
  14. 小程序:uniapp开发和原生开发语法区别
  15. word设置表格文字紧贴下框线
  16. 做机器人开发,你一定绕不开的模块!
  17. GRE词汇整理(magoosh版本)
  18. CSS cursor(鼠标状态)属性
  19. chi2inv函数 matlab_matlab工具箱函数汇总(转)
  20. Android人生整理第二章:Java编程第一节-对象导论

热门文章

  1. 中国的“程序员相轻”
  2. Linux杀死指定端口的进程
  3. 机器人无限火力无限e符文_LOLs10无限火力蒸汽机器人布里茨ap输出装符文玩法攻略...
  4. 【谷粒商城】分布式事务与下单
  5. 【jiasuba】鲜为人知的Word双行合一设置
  6. 创建一个简单的Python服务器
  7. 京东评论接口,item_review - 获得JD商品评论接口接入参数解决方案
  8. ios 分享功能 集成友盟分享
  9. 最简单实用的任意字符串隐藏,身份证号隐藏,手机号隐藏方法
  10. Word2016取消首字母大写