Memento模式

备忘录模式最常见的应用是各种编辑器,如果写错了,点击“撤销”按钮就能回到原来的状态。

不使用备忘录模式对实例进行保存和恢复,很容易破坏封装性:将依赖实例内部结构的代码写得到处都是,程序变得难以维护。

备忘录模式专门添加了Memento角色,这个角色专门用来保存和恢复实例,能有效防止对象的封装性遭破坏。

示例代码

下面这段代码演示备忘录模式的用法,代码的主要功能是:

模拟一个掷骰子游戏,规则:

  • 结果为1,加100块钱
  • 结果为2,减一半钱
  • 结果为6,获得一个水果
  • 结果为其他,什么也没有

如果钱增加了,则使用备忘录记下来

如果钱连续两次减少,则恢复到第一次减钱之前的状态

程序类图

代码

Memento

public class Memento {
int money; // 所持金钱
ArrayList fruits; // 当前获得的水果
public int getMoney() { // 获取当前所持金钱(narrow interface)
return money;
}
Memento(int money) { // 构造函数(wide interface)
this.money = money;
this.fruits = new ArrayList();
}
void addFruit(String fruit) { // 添加水果(wide interface)
fruits.add(fruit);
}
List getFruits() { // 获取当前所持所有水果(wide interface)
//浅复制,只复制引用。
return (List)fruits.clone();
}
}

Gamer

public class Gamer {
private int money; // 所持金钱
private List fruits = new ArrayList(); // 获得的水果
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 it = fruits.iterator();
while (it.hasNext()) {
String f = (String)it.next();
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() {
return fruitsname[random.nextInt(fruitsname.length)];
}
}

Main

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("");
}
}
}

结果

  • 连续两次减钱,则恢复到第1次减钱之前
  • 如果加钱,则记录状态
...省略
==== 8
当前状态:[money = 100, fruits = [橘子]]
所持金钱增加了。
所持金钱为200元。
(所持金钱增加了许多,因此保存游戏当前的状态) ==== 9
当前状态:[money = 200, fruits = [橘子]]
所持金钱减半了。
所持金钱为100元。 ==== 10
当前状态:[money = 100, fruits = [橘子]]
所持金钱减半了。
所持金钱为50元。
(所持金钱减少了许多,因此将游戏恢复至以前的状态) ==== 11
当前状态:[money = 200, fruits = [橘子]]
获得了水果(葡萄)。
所持金钱为200元。
...省略

角色和类图

模式类图

角色

  • Originator(生成者)

    主要关注createMementorestoreMemento两个方法,前者是保存快照,后者是恢复快照。本例中由Gamer扮演此角色。

  • Memento(备忘录)

    这个角色用来保存Originator的信息,我们要注意其方法的访问权限控制

    这个角色内部有宽接口和窄接口两种接口,宽和窄指的是内部结构的对外暴露程度。宽接口只能提供给Originator,窄接口提供给外部角色。

    这样控制的目的是防止使用者破坏封装。

    在Originator和Memento之间不存在什么封装,他们俩也深深耦合。这无法避免,他们俩都在围绕着共同的属性集做文章,事实上他们俩本可以合为一体。那为什么拆出来?因为职责分离。

  • Caretaker

    就是负责保存和恢复工作的角色,在本例中由Main函数扮演此角色。

    需要注意的是:为了不破坏封装性,Memento暴露给Caretaker的内容很少,少到刚好够Caretaker使用。

    比如本例中Main函数的判断条件需要money的值,Memento就只暴露一个getMoney方法,其他的一点不多给。

思路拓展

接口可见性

本例的代码结构如图:

java的方法可见性规定

可见性 说明
public 所有类都可以访问
protected 同一个包内或该类的子类可以访问
同一个包内的类可以访问
private 只有该类才能访问

Memento类中的可见性

可见性 成员 谁可以访问
money Memento、Gamer
fruits Memento、Gamer
public getMoney Memento、Gamer、Main
Memento Memento、Gamer
addFruit Memento、Gamer
getFruits Memento、Gamer

只有getMoney方法可以被包外的类访问,它是一个窄接口,“窄”指的是此接口能操作的类内部的内容很少。

通过对方法的访问权限的控制,可以提升封装性,减少内部结构对外暴露引发的一系列问题。

保存多少个Memento

比如word软件,支持多次撤销功能,就需要保存多个Memento。可以用集合来存储。

划分Caretaker和Originator的意义

还是职责分离

Originator负责保存和恢复的具体实现。

Caretaker负责执行保存和恢复动作。

如果需求变化了,要求

  • 支持多次撤销
  • 不仅可以多次撤销,还能保存到文件或数据库

此时就只需要修改Originator即可,不必修改Caretaker。

《图解设计模式》读书笔记8-2 MEMENTO模式的更多相关文章

  1. HeadFirst设计模式读书笔记(3)-装饰者模式(Decorator Pattern)

    装饰者模式:动态地将责任附件到对象上.若要扩展功能,装饰者提东了比继承更有弹性的替代方案. 装饰者和被装饰对象有相同的超类型 你可以用一个或者多个装饰者包装一个对象. 既然装饰者和被装饰对象有相同的超 ...

  2. HeadFirst设计模式读书笔记--目录

    HeadFirst设计模式读书笔记(1)-策略模式(Strategy Pattern) HeadFirst设计模式读书笔记(2)-观察者模式(Observer Pattern) HeadFirst设计 ...

  3. Head First 设计模式读书笔记(1)-策略模式

    一.策略模式的定义 策略模式定义了算法族,分别封装起来,让它们之间可以互换替换,此模式让算法的变化独立使用算法的客户. 二.使用策略模式的一个例子 2.1引出问题 某公司做了一套模拟鸭子的游戏:该游戏 ...

  4. C#设计模式学习笔记:(22)备忘录模式

    本笔记摘抄自:https://www.cnblogs.com/PatrickLiu/p/8176974.html,记录一下学习过程以备后续查用. 一.引言 今天我们要讲行为型设计模式的第十个模式--备 ...

  5. JavaScript设计模式:读书笔记(未完)

    该篇随我读书的进度持续更新阅读书目:<JavaScript设计模式> 2016/3/30 2016/3/31 2016/4/8 2016/3/30: 模式是一种可复用的解决方案,可用于解决 ...

  6. 图解http读书笔记

    以前对HTTP协议一知半解,一直不清楚前端需要对于HTTP了解到什么程度,知道接触的东西多了,对于性能优化.服务端的配合和学习中也渐渐了解到了HTTP基础的重要性,看了一些大神对HTTP书籍的推荐,也 ...

  7. Java设计模式学习笔记(二) 简单工厂模式

    前言 本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址 正文开始... 1. 简介 简单工厂模式不属于GoF23中设计模式之一,但在软件开发中应用也较为 ...

  8. Java设计模式学习笔记(三) 工厂方法模式

    前言 本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址 1. 简介 上一篇博客介绍了简单工厂模式,简单工厂模式存在一个很严重的问题: 就是当系统需要引入 ...

  9. Java设计模式学习笔记(四) 抽象工厂模式

    前言 本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址 1. 抽象工厂模式概述 工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问 ...

  10. C#设计模式学习笔记:(23)解释器模式

    本笔记摘抄自:https://www.cnblogs.com/PatrickLiu/p/8242238.html,记录一下学习过程以备后续查用. 一.引言 今天我们要讲行为型设计模式的第十一个模式-- ...

随机推荐

  1. Hangfire

    参考 开源分布式Job系统,调度与业务分离-如何创建一个计划HttpJob任务

  2. 自然语言处理资源NLP

    转自:https://github.com/andrewt3000/DL4NLP Deep Learning for NLP resources State of the art resources ...

  3. 欧拉降幂公式 Super A^B mod C

    Description Given A,B,C, You should quickly calculate the result of A^B mod C. (1<=A,C<=100000 ...

  4. 为什么我选择用 flutter

    1. flutter 生成的是机器代码,他既不是 hybrid 也不是transpiler,  因此有很高的执行效率. 2. declarative ui,这不是什么新的概念,在 react vue ...

  5. 如何设置Linux虚拟机的IP地址

    本文会详细的解释如何在Linux虚拟机下设置IP地址 我的虚拟机是CentOS 首先,打开你的虚拟机 1.修改主机名 修改完主机名之后,别忘了用:wq命令保存退出 然后我们来设置虚拟机的IP地址 首先 ...

  6. Makefile中$$的使用

    在linux的Makefile中,经常会见到$var和$$var的形式.下面就这两种表示方法的区别进行简单的概述. 在Makefile中的规则命令行中: $var:将Makefile中的变量var的值 ...

  7. gradle配置国内阿里云镜像

    对单个项目生效,在项目中的build.gradle修改内容 buildscript { repositories { maven { url 'http://maven.aliyun.com/nexu ...

  8. Linux中查看某 个软件的安装路径

    本人qq群也有许多的技术文档,希望可以为你提供一些帮助(非技术的勿加). QQ群:   281442983 (点击链接加入群:http://jq.qq.com/?_wv=1027&k=29Lo ...

  9. 7天玩转性能&接口测试

    众所周知,近10年IT领域有两个关键的风向转变,传统IT向云计算转变,传统瀑布和迭代开发模式向敏捷开发模式转变.这两个转变促成了DevOps产品交付模式的出现.互联网行业竞争激烈,许多公司专注于产品和 ...

  10. Activiti的流程实例及挂起激活(七)

    1.1什么是流程实例 参与者(可以是用户也可以是程序)按照流程定义内容发起一个流程,这就是一个流程实例.是动态的.流程定义和流程实例的图解: 1.2启动流程实例 流程定义部署在 activiti 后, ...