设计模式(十八)Memento模式
在使用面向对象编程的方式实现撤销功能时,需要事先保存实例的相关状态信息。然后,在撤销时,还需要根据所保存的信息将实例恢复至原来的状态。
要想恢复实例,需要一个可以自由访问实例内部结构的权限。但是,如果稍有不注意,又可能会将依赖于实例内部结构的代码分散地编写在程序的各种地方,导致程序变得难以维护。这种情况就叫做“破坏了封装性”。
通过引入表示实例状态的角色,可以在保存和恢复实例时有效地防止对象的封装性遭到破坏。这就是Memento模式。
使用Memento可以实现撤销、重做、历史记录、快照等功能。它事先将某个时间点的实例的状态保存下来,之后在有必要时,再将实例恢复至当时的状态。
首先看一下示例程序的类图。

这个示例程序时一个收集水果和获取金钱数的掷骰子游戏,游戏规则如下:
1.游戏是自动进行。
2.游戏的主人公通过掷骰子来决定下一个状态。
3.骰子点数为1,金钱增加。
4.骰子点数为2,金钱减少。
5.骰子点数为6,得到水果。
6.主人公没钱的时候游戏结束。
在程序中,如果金钱增加,为了方便将来恢复状态,会生成Memento实例,将现在的状态保存起来。所保存的数据为当前只有的金钱和水果。如果不断掷骰子导致金钱减少,为了防止金钱变为0而结束游戏,我们会使用Memento实例将游戏恢复至之前的状态。
package bigjunoba.bjtu.game;
import java.util.*;
public class Memento {
int money; // 所持金钱
ArrayList<String> fruits;
// 当前获得的水果
public int getMoney() { // 获取当前所持金钱(narrow interface)
return money;
}
Memento(int money) { // 构造函数(wide interface)
this.money = money;
this.fruits = new ArrayList<String>();
}
void addFruit(String fruit) { // 添加水果(wide interface)
fruits.add(fruit);
}
@SuppressWarnings("unchecked")
List<String> getFruits() { // 获取当前所持所有水果(wide interface)
return (List<String>)fruits.clone();
}
}
Memento类。这里没有将money和fruits的可见性设为private,是因为希望同在game包下的Gamer类可以访问者两个字段。Memento构造函数也不是public,因此并不是任何其他类都可以生成Memento类的实例。只有在同一个包下的类(即Gamer类)才能调用Memento类的构造函数。addFruit方法也不是public,还有getFruits方法也不是public,这个是因为只有同一个包下的其他类才能添加水果,无法从game包外部改变Memento内部的状态。
package bigjunoba.bjtu.game;
import java.util.*; public class Gamer {
private int money; // 所持金钱
private List<String> fruits = new ArrayList<String>(); // 获得的水果
private Random random = new Random(); // 随机数生成器
private static String[] fruitsname = { // 表示水果种类的数组
"苹果", "葡萄", "香蕉", "橘子",
}; public Gamer(int money) { // 构造函数
this.money = money;
}
public int getMoney() { // 获取当前所持金钱
return money;
} public void bet() { // 投掷骰子进行游戏
int dice = random.nextInt(6) + 1; // 掷骰子
if (dice == 1) { // 骰子结果为1…增加所持金钱
money += 100;
System.out.println("所持金钱增加了。");
} else if (dice == 2) { // 骰子结果为2…所持金钱减半
money /= 2;
System.out.println("所持金钱减半了。");
} else if (dice == 6) { // 骰子结果为6…获得水果
String f = getFruit();
System.out.println("获得了水果(" + f + ")。");
fruits.add(f);
} else { // 骰子结果为3、4、5则什么都不会发生
System.out.println("什么都没有发生。");
}
} public Memento createMemento() { // 拍摄快照
Memento m = new Memento(money);
Iterator<String> it = fruits.iterator();
while (it.hasNext()) {
String f = (String)it.next();
if (f.startsWith("好吃的")) { // 只保存好吃的水果
m.addFruit(f);
}
}
return m;
}
public void restoreMemento(Memento memento) { // 撤销
this.money = memento.money;
this.fruits = memento.getFruits();
} public String toString() { // 用字符串表示主人公状态
return "[money = " + money + ", fruits = " + fruits + "]";
} private String getFruit() { // 获得一个水果
String prefix = "";
if (random.nextBoolean()) {
prefix = "好吃的";
}
return prefix + fruitsname[random.nextInt(fruitsname.length)];
}
}
Gamer类是表示游戏主人公的类。进行游戏的主要方法时bet方法。createMemento方法是主要的方法,作用是保存当前的状态(快照)。根据当前时间点所持有的金钱和水果生成一个Memento类的实例,该实例代表了“当前Gamer的状态”,它会被返回给调用者。restoreMemento类作为撤销方法,它会根据接收到的Memento类的实例来讲Gamer恢复为以前的状态。
package bigjunoba.bjtu.test;
import bigjunoba.bjtu.game.*;
public class Main {
public static void main(String[] args) {
Gamer gamer = new Gamer(100); // 最初的所持金钱数为100
Memento memento = gamer.createMemento(); // 保存最初的状态
for (int i = 0; i < 100; i++) {
System.out.println("==== " + i); // 显示掷骰子的次数
System.out.println("当前状态:" + gamer); // 显示主人公现在的状态
gamer.bet(); // 进行游戏
System.out.println("所持金钱为" + gamer.getMoney() + "元。");
// 决定如何处理Memento
if (gamer.getMoney() > memento.getMoney()) {
System.out.println(" (所持金钱增加了许多,因此保存游戏当前的状态)");
memento = gamer.createMemento();
} else if (gamer.getMoney() < memento.getMoney() / 2) {
System.out.println(" (所持金钱减少了许多,因此将游戏恢复至以前的状态)");
gamer.restoreMemento(memento);
}
// 等待一段时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("");
}
}
}
Main类为测试类,在变量memento中保存了“某个时间点的Gamer的状态”。如果运气很好,金钱增加了,就保存现在的状态,如果运气不好,金钱不足了,就会恢复到memento状态。
==== 0
当前状态:[money = 100, fruits = []]
所持金钱增加了。
所持金钱为200元。
(所持金钱增加了许多,因此保存游戏当前的状态) ==== 1
当前状态:[money = 200, fruits = []]
获得了水果(葡萄)。
所持金钱为200元。 ==== 2
当前状态:[money = 200, fruits = [葡萄]]
获得了水果(好吃的橘子)。
所持金钱为200元。 ==== 3
当前状态:[money = 200, fruits = [葡萄, 好吃的橘子]]
什么都没有发生。
所持金钱为200元。 ==== 4
当前状态:[money = 200, fruits = [葡萄, 好吃的橘子]]
所持金钱减半了。
所持金钱为100元。 ==== 5
当前状态:[money = 100, fruits = [葡萄, 好吃的橘子]]
获得了水果(苹果)。
所持金钱为100元。 ==== 6
当前状态:[money = 100, fruits = [葡萄, 好吃的橘子, 苹果]]
所持金钱减半了。
所持金钱为50元。
(所持金钱减少了许多,因此将游戏恢复至以前的状态) ==== 7
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。 ==== 8
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。 ==== 9
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。 ==== 10
当前状态:[money = 200, fruits = []]
所持金钱减半了。
所持金钱为100元。 ==== 11
当前状态:[money = 100, fruits = []]
什么都没有发生。
所持金钱为100元。 ==== 12
当前状态:[money = 100, fruits = []]
所持金钱增加了。
所持金钱为200元。 ==== 13
当前状态:[money = 200, fruits = []]
所持金钱减半了。
所持金钱为100元。 ==== 14
当前状态:[money = 100, fruits = []]
什么都没有发生。
所持金钱为100元。 ==== 15
当前状态:[money = 100, fruits = []]
所持金钱减半了。
所持金钱为50元。
(所持金钱减少了许多,因此将游戏恢复至以前的状态) ==== 16
当前状态:[money = 200, fruits = []]
所持金钱减半了。
所持金钱为100元。 ==== 17
当前状态:[money = 100, fruits = []]
获得了水果(香蕉)。
所持金钱为100元。 ==== 18
当前状态:[money = 100, fruits = [香蕉]]
所持金钱减半了。
所持金钱为50元。
(所持金钱减少了许多,因此将游戏恢复至以前的状态) ==== 19
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。 ==== 20
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。
测试结果。这个测试结果有点非酋,看懂就行了。

示例程序的时序图。

Memento模式的类图。






设计模式(十八)Memento模式的更多相关文章
- 【转】设计模式 ( 十八 ) 策略模式Strategy(对象行为型)
设计模式 ( 十八 ) 策略模式Strategy(对象行为型) 1.概述 在软件开发中也常常遇到类似的情况,实现某一个功能有多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成 ...
- 设计模式 ( 十八 ) 策略模式Strategy(对象行为型)
设计模式 ( 十八 ) 策略模式Strategy(对象行为型) 1.概述 在软件开发中也经常遇到类似的情况,实现某一个功能有多种算法或者策略,我们能够依据环境或者条件的不同选择不同的算法或者策略来完毕 ...
- 设计模式 ( 十四 ) 迭代器模式Iterator(对象行为型)
设计模式 ( 十四 ) 迭代器模式Iterator(对象行为型) 1.概述 类中的面向对象编程封装应用逻辑.类,就是实例化的对象,每个单独的对象都有一个特定的身份和状态.单独的对象是一种组织代码的 ...
- 设计模式 ( 十九 ) 模板方法模式Template method(类行为型)
设计模式 ( 十九 ) 模板方法模式Template method(类行为型) 1.概述 在面向对象开发过程中,通常我们会遇到这样的一个问题:我们知道一个算法所需的关键步骤,并确定了这些步骤的执行 ...
- C++设计模式实现--备忘录(Memento)模式
一. 备忘录模式 定义:在不破坏封装性的前提下,捕获一个对象的内部状态.并在该对象之外保存这个状态. 这样以后就可将该对象恢复到原先保存的状态. 结构图: 使用范围: Memento 模式比較适用于功 ...
- 设计模式 ( 十八 ):State状态模式 -- 行为型
1.概述 在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理.最直接的解决方案是将这些所有可能发生的情况全都考虑到.然后使用if... ellse语句来做状态判断来进行不同情况的处理.但是对 ...
- C#设计模式之十八状态模式(State Pattern)【行为型】
一.引言 今天我们开始讲“行为型”设计模式的第六个模式,该模式是[状态模式],英文名称是:State Pattern.无论是现实世界,还是面向对象的OO世界,里面都有一个东西,那就是对象.有对象当然就 ...
- java常用设计模式十:模板模式
一.定义 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中.模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤. 如果上面的话不好理解,请看下面的例子 二.示例 1)定义一个模 ...
- 《Head First 设计模式》[01] 策略模式
<Head First 设计模式>(点击查看详情) 1.写在前面的话 之前在列书单的时候,看网友对于设计模式的推荐里说,设计模式的书类别都大同小异,于是自己就选择了Head First系列 ...
- Java 设计模式系列(十八)备忘录模式(Memento)
Java 设计模式系列(十八)备忘录模式(Memento) 备忘录模式又叫做快照模式(Snapshot Pattern)或Token模式,是对象的行为模式.备忘录对象是一个用来存储另外一个对象内部状态 ...
随机推荐
- C#控件及常用属性
1.窗体(Form) 1.常用属性 (1)Name 属性:用来获取或设置窗体的名称,在应用程序中可通过Name 属性来引用窗体. (2) WindowState 属性: 用来获取或设置窗体的窗口状态. ...
- 基于Docker搭建大数据集群(七)Hbase部署
基于Docker搭建大数据集群(七)Hbase搭建 一.安装包准备 Hbase官网下载 微云下载 | 在 tar 目录下 二.版本兼容 三.角色分配 节点 Master Regionserver cl ...
- linux常用开发命令总结
linux常用命令 文件操作命令 1. cd 目录名/目录名 切换目录 cd .. 切换到上一级目录 (change dictionary) Ctrl+C强制退出命令行,回到上一级 2.ls ...
- 又写了两个实用的微信小程序
忙里偷闲,最近又写了两个小程序. 一个是手机壁纸小程序,名字叫[来搜图],特点是界面干净清爽,没有多余的东西.开发这个是因为讨厌市面上那些壁纸app那样那么多的广告,真的太影响体验了.而且小程序更加轻 ...
- C#刷遍Leetcode面试题系列连载(2): No.38 - 报数
目录 前言 题目描述 相关话题 相似题目 解题思路: 运行结果: 代码要点: 参考资料: 文末彩蛋 前言 前文传送门: C# 刷遍 Leetcode 面试题系列连载(1) - 入门与工具简介 上篇文章 ...
- PHP的bcmath编译安装
问题描述:zabbix编译安装时PHP条件检查失败,如下图: 解决办法: 1.PHP自带bcmath扩展模块,可直接进行编译安装. [root@localhost etc]# cd /usr/loca ...
- ES6——箭头函数与普通函数的区别
ES6标准新增了一种新的函数:Arrow Function(箭头函数). 为什么叫Arrow Function?因为它的定义用的就是一个箭头: 语法: //1.没有形参的时候 let fun = () ...
- .net core运用application/x-www-form-urlencoded发起post请求
通常情况下都是使用 application/json发起post请求,但是有的.net 接口只接收 application/x-www-form-urlencoded 例如: { name:" ...
- jQuery.noConflict()解决imgBox.js依赖jquery版本问题
jQuery提供两种点击图片放大效果出处 在使用imgbox.js是出现的jquery版本不兼容问题,之后了解到jQuery.noConflict()的用法 jQuery.noConflict()的存 ...
- Springboot2.x + ShardingSphere 实现分库分表
之前一篇文章中我们讲了基于Mysql8的读写分离(文末有链接),这次来说说分库分表的实现过程. 概念解析 垂直分片 按照业务拆分的方式称为垂直分片,又称为纵向拆分,它的核心理念是专库专用. 在拆分之前 ...