设计模式(十八)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模式,是对象的行为模式.备忘录对象是一个用来存储另外一个对象内部状态 ...
随机推荐
- Spring boot 梳理 - mappingJackson2JsonView
MappingJacksonJsonView已被废弃了: http://static.javadoc.io/org.springframework/spring-webmvc/4.0.1.RELEAS ...
- Angular toastr提示框
1. 安装ngx-toastr包 npm install ngx-toastr --save 2. package.json中引入toastr样式文件 "styles": [&qu ...
- Pots POJ 3414
/* *POJ 3414 *简单模板bfs *编程应该为了方便理解,尽量提供接口 */ #include<cstdio> #include<algorithm> #includ ...
- Go中使用seed得到相同随机数的问题
1. 重复的随机数 废话不多说,首先我们来看使用seed的一个很神奇的现象. func main() { for i := 0; i < 5; i++ { rand.Seed(time.Now( ...
- 触电JavaScript-如何将json 二维数组转换为 JSON object
最近因为项目中使用的是 ActiveReports .Net 产品,因为他们最近新出了 ActiveReports JS 版本,所以内心有点痒痒,想试试这个纯前端版本报表控件到底如何,毕竟我们项目有 ...
- 从零开始的vue学习笔记(二)
数据与方法 当一个 Vue 实例被创建时,它将 data 对象中的所有的属性加入到 Vue 的响应式系统中.data的数据和视图同步更新. 实例创建后添加一个新的属性,对这个属性的的改动将不会触发任何 ...
- [Python] Python 学习记录(1)
1.概论 弱类型 一个变量能被赋值时能与原类型不同 x = 1 x = "1" #不加分号,给x赋值为int后再次赋值为string是可行的 与或非 and or not / ...
- redis mysql 连接池 之 golang 实现
1 mysql 连接池代码 package lib import ( "database/sql" "fmt" "strconv" &quo ...
- F#周报2019年第41期
新闻 .NET架构指南 美妙的WebSharper:学术刊物 .NET Core 3.0中Blazor Server的方案与性能 Mono 6.4.0发布说明 CapitolFSharp召集发言人 视 ...
- JAVAWEB第一节课的课后思考
第一开发一个网站需要的一些技术 至少熟悉一种建站程序.(html,javascript等等)对空间和域名的知识有一定的了解.有一些美工基础(例如ps设计等等).对编程有一些了解.HTML的代码知识基本 ...