设计模式的征途—20.备忘录(Memento)模式
相信每个人都有后悔的时候,但是人生并无后悔药,有些错误一旦发生就无法再挽回,有些事一旦错过就不会再重来,有些话一旦说出口也就不可能再收回,这就是人生。为了不让自己后悔,我们总是需要三思而后行。这里我们要学习一种可以在软件中实现后悔机制的设计模式—备忘录模式,它是软件中的“后悔药”。
| 备忘录模式(Memento) | 学习难度:★★☆☆☆ | 使用频率:★★☆☆☆ |
一、可悔棋的中国象棋游戏
Background:M公司欲开发一款可以运行在Android平台的触摸式中国象棋软件,如下图所示。由于考虑到有些用户是新手,经常不小心走错棋;还有些用户因为不习惯使用手指在手机屏幕上拖动棋子,常常出现操作失误,因此该中国象棋软件要提供“悔棋”功能,用户走错棋或操作失误后可恢复到前一个步骤。
如何实现“悔棋”功能是M公司开发人员需要面对的一个重要问题。“悔棋”就是让系统恢复到某个历史状态,在很多软件中称之为“撤销”。
在实现撤销时,首先需要保存系统的历史状态,当用户需要取消错误操作并且返回到某个历史状态时,可以取出事先保存的历史状态来覆盖当前状态,如下图所示。

备忘录正是为解决此类撤销问题而诞生,它为软件提供了“后悔药”。
二、备忘录模式概述
2.1 备忘录模式简介
备忘录模式提供了一种状态恢复的机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂存的备忘录将状态恢复。
备忘录(Memento)模式:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token。
2.2 备忘录模式结构
备忘录模式的核心在于备忘录类以及用于管理备忘录的负责任类的设计,其结构如下图所示:

(1)Originator(原发器):它是一个普通类,可以创建一个备忘录,并存储其当前内部状态,也可以使用备忘录来恢复其内部状态,一般需要保存内部状态的类设计为原发器。
(2)Memento(备忘录):存储原发器的状态,根据原发器来决定保存哪些内部状态。
(3)Caretaker(负责任):负责任又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。
三、可悔棋的中国象棋实现
3.1 基本设计结构
为了实现撤销功能,M公司开发人员决定使用备忘录模式来设计中国象棋,其基本结构如下图所示:

其中,Chessman充当原发器,ChessmanMemento充当备忘录,而MementoCaretaker充当负责人,在MementoCaretaker中定义了一个ChessmanMemento的对象,用于存储备忘录。
3.2 具体代码实现
(1)原发器:Chessman
/// <summary>
/// 原发器:Chessman
/// </summary>
public class Chessman
{
public string Label { get; set; }
public int X { get; set; }
public int Y { get; set; } public Chessman(string label, int x, int y)
{
Label = label;
X = x;
Y = y;
} // 保存状态
public ChessmanMemento Save()
{
return new ChessmanMemento(Label, X, Y);
} // 恢复状态
public void Restore(ChessmanMemento memento)
{
Label = memento.Label;
X = memento.X;
Y = memento.Y;
}
}
(2)备忘录:ChessmanMemento
/// <summary>
/// 备忘录:ChessmanMemento
/// </summary>
public class ChessmanMemento
{
public string Label { get; set; }
public int X { get; set; }
public int Y { get; set; } public ChessmanMemento(string label, int x, int y)
{
Label = label;
X = x;
Y = y;
}
}
(3)负责人:MementoCaretaker
/// <summary>
/// 负责人:MementoCaretaker
/// </summary>
public class MementoCaretaker
{
public ChessmanMemento Memento { get; set; }
}
(4)客户端测试
public static void Main()
{
MementoCaretaker mc = new MementoCaretaker();
Chessman chess = new Chessman("车", , );
Display(chess);
// 保存状态
mc.Memento = chess.Save();
chess.Y = ;
Display(chess);
// 保存状态
mc.Memento = chess.Save();
Display(chess);
chess.X = ;
Display(chess); Console.WriteLine("---------- Sorry,俺悔棋了 ---------"); // 恢复状态
chess.Restore(mc.Memento);
Display(chess);
}
这里定义了一个辅助显示的方法Display
public static void Display(Chessman chess)
{
Console.WriteLine("棋子 {0} 当前位置为:第 {1} 行 第 {2} 列", chess.Label, chess.X, chess.Y);
}
编译运行后结果如下图所示:

3.3 多次撤销重构
刚刚我们实现的是单次撤销,那么如果要实现多次撤销呢?这里我们在负责人类中将原来的单一对象改为集合来存储多个备忘录,每个备忘录负责保存一个历史状态,在撤销时可以对备忘录集合进行逆向遍历,回到一个指定的历史状态,而且还可以对备忘录集合进行正向遍历,实现重做(ReDo)或恢复操作。
这里我们设计一个新的负责人类NewMementoCaretaker类进行小修改,其代码如下:
/// <summary>
/// 负责人:NewMementoCaretaker
/// </summary>
public class NewMementoCaretaker
{
private IList<ChessmanMemento> mementoList = new List<ChessmanMemento>(); public ChessmanMemento GetMemento(int i)
{
return mementoList[i];
} public void SetMemento(ChessmanMemento memento)
{
mementoList.Add(memento);
}
}
客户端测试代码如下:
private static int index = -;
private static NewMementoCaretaker mementoCaretaker = new NewMementoCaretaker(); public static void Main()
{
Chessman chess = new Chessman("车", , );
Play(chess);
chess.Y = ;
Play(chess);
chess.X = ;
Play(chess); Undo(chess, index);
Undo(chess, index);
Redo(chess, index);
Redo(chess, index);
} // 下棋
public static void Play(Chessman chess)
{
// 保存备忘录
mementoCaretaker.SetMemento(chess.Save());
index++; Console.WriteLine("棋子 {0} 当前位置为 第 {1} 行 第 {2} 列", chess.Label, chess.X, chess.Y);
} // 悔棋
public static void Undo(Chessman chess, int i)
{
Console.WriteLine("---------- Sorry,俺悔棋了 ---------");
index--;
// 撤销到上一个备忘录
chess.Restore(mementoCaretaker.GetMemento(i - )); Console.WriteLine("棋子 {0} 当前位置为 第 {1} 行 第 {2} 列", chess.Label, chess.X, chess.Y);
} // 撤销悔棋
public static void Redo(Chessman chess, int i)
{
Console.WriteLine("---------- Sorry,撤销悔棋 ---------");
index++;
// 恢复到下一个备忘录
chess.Restore(mementoCaretaker.GetMemento(i + )); Console.WriteLine("棋子 {0} 当前位置为 第 {1} 行 第 {2} 列", chess.Label, chess.X, chess.Y);
}
编译运行后的结果如下图所示:

四、备忘录模式小结
4.1 主要优点
(1)提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤。
(2)实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。
4.2 主要缺点
资源消耗过大,资源消耗过大,资源消耗过大 => 说三遍!因为每保存一次对象状态都需要消耗一定系统资源。
4.3 应用场景
(1)需要保存一个对象在某一个时刻的全部状态或部分状态状态,以便需要在后面需要时可以恢复到先前的状态。
(2)防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。
参考资料

刘伟,《设计模式的艺术—软件开发人员内功修炼之道》
设计模式的征途—20.备忘录(Memento)模式的更多相关文章
- 设计模式C++描述----17.备忘录(Memento)模式
一. 备忘录模式 定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态.这样以后就可将该对象恢复到原先保存的状态. 结构图: 使用范围: Memento 模式比较适用于功能 ...
- 设计模式之第20章-访问者模式(Java实现)
设计模式之第20章-访问者模式(Java实现) “嘿,你脸好红啊.”“精神焕发.”“怎么又黄了?”“怕冷,涂的,涂的,蜡.”“身上还有酒味,露馅了吧,原来是喝酒喝的啊.”“嘿嘿,让,让你发现了,今天来 ...
- C++设计模式实现--备忘录(Memento)模式
一. 备忘录模式 定义:在不破坏封装性的前提下,捕获一个对象的内部状态.并在该对象之外保存这个状态. 这样以后就可将该对象恢复到原先保存的状态. 结构图: 使用范围: Memento 模式比較适用于功 ...
- 设计模式(十八)Memento模式
在使用面向对象编程的方式实现撤销功能时,需要事先保存实例的相关状态信息.然后,在撤销时,还需要根据所保存的信息将实例恢复至原来的状态. 要想恢复实例,需要一个可以自由访问实例内部结构的权限.但是,如果 ...
- 备忘录(Memento)模式
备忘录模式又叫做快照模式或者Token模式. 备忘录对象是一个用来存储另一个对象内部状态的快照的对象.备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉住,并外部化,存储起来,从而可以在将来 ...
- 《图解设计模式》读书笔记8-2 MEMENTO模式
目录 Memento模式 示例代码 程序类图 代码 角色和类图 模式类图 角色 思路拓展 接口可见性 保存多少个Memento 划分Caretaker和Originator的意义 Memento模式 ...
- Java设计模式之从[暗黑破坏神存档点]分析备忘录(Memento)模式
在大部分游戏中,都有一个"存档点"的概念.比如,在挑战boss前,游戏会在某个地方存档,假设玩家挑战boss失败,则会从这个存档点開始又一次游戏.因此,我们能够将这个"存 ...
- 设计模式C++描述----20.迭代器(Iterator)模式
一. 举例说明 我们知道,在 STL 里提供 Iterator 来遍历 Vector 或者 List 数据结构. Iterator 模式也正是用来解决对一个聚合对象的遍历问题,将对聚合的遍历封装到一个 ...
- 设计模式的征途(C#实现)—文章目录索引
1.预备篇 UML类图10分钟快速入门 2.创建型模式 ① 设计模式的征途-01.单例(Singleton)模式 ② 设计模式的征途-02.简单工厂(Simple Factory)模式 ③ 设计模式的 ...
随机推荐
- App 组件化/模块化之路——如何封装网络请求框架
App 组件化/模块化之路——如何封装网络请求框架 在 App 开发中网络请求是每个开发者必备的开发库,也出现了许多优秀开源的网络请求库.例如 okhttp retrofit android-asyn ...
- 玲珑杯 Round #11 (1001 1004 1007)
比赛链接 直接贴代码.. #include<bits/stdc++.h> using namespace std; typedef long long LL; int main() { L ...
- 学会用requirejs,5分钟足矣
学会用requirejs,5分钟足矣 据说公司的项目较多的用到requirejs管理依赖,所以大熊同学挤出了5分钟休息时间学习了一下,现在分享一下.如果你想了解requirejs的实现原理,请绕道!如 ...
- linux下 git 安装
1.使用yum安装 yum -y install git yum remove git 2.源代码安装 a.下载git源码 网址为 https://github.com/git/git/releas ...
- (转)Java线程:新特征-线程池
Java线程:新特征-线程池 Sun在Java5中,对Java线程的类库做了大量的扩展,其中线程池就是Java5的新特征之一,除了线程池之外,还有很多多线程相关的内容,为多线程的编程带来了极大便利 ...
- Eclipse详细设置护眼背景色和字体颜色并导出
Eclipse详细设置护眼背景色和字体颜色并导出 Eclipse是一款码农们喜闻乐见的集成开发平台,但是其默认的主题和惨白的背景色实在是太刺激眼球了.下面,将给大家详细介绍如何设置成护眼主题的方法,也 ...
- if else 和switch case以及continue,break的区别
1,if 经常用于做区间判断 或者 固定值: break和continue的使用 break:用来结束循环结构或者switch case continue:结束此次循环进入 ...
- mysql 5.7 root密码重置(centos 7)
mysql5.7版本之后,与mariadb不同,在安装之后,在启动之时,会进行自动随机密码的设定,所以在systemctl start mysqld之后,会出现mysql -uroot -p无法登陆的 ...
- year:2017 month:07 day:31
2017-07-31 JAVA se 1:基础 控制语句:continue语句:退出本次循环 break语句:退出当前循环 循环语句:for(初始化:条件表达式:循环体){循环语句} 先初始化,再执行 ...
- JavaScript语言精粹-读书笔记
前言:很久之前读过一遍该书,近日得闲,重拾该书,详细研究一方,欢迎讨论指正. 目录: 1.精华 2.语法 3.对象 4.函数 5.继承 6.数组 7.正则表达式 8.方法 9.代码风格 10.优美的特 ...
