相信每个人都有后悔的时候,但是人生并无后悔药,有些错误一旦发生就无法再挽回,有些事一旦错过就不会再重来,有些话一旦说出口也就不可能再收回,这就是人生。为了不让自己后悔,我们总是需要三思而后行。这里我们要学习一种可以在软件中实现后悔机制的设计模式—备忘录模式,它是软件中的“后悔药”。

备忘录模式(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)防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。

参考资料

  

  刘伟,《设计模式的艺术—软件开发人员内功修炼之道》

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

设计模式的征途—20.备忘录(Memento)模式的更多相关文章

  1. 设计模式C++描述----17.备忘录(Memento)模式

    一. 备忘录模式 定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态.这样以后就可将该对象恢复到原先保存的状态. 结构图: 使用范围: Memento 模式比较适用于功能 ...

  2. 设计模式之第20章-访问者模式(Java实现)

    设计模式之第20章-访问者模式(Java实现) “嘿,你脸好红啊.”“精神焕发.”“怎么又黄了?”“怕冷,涂的,涂的,蜡.”“身上还有酒味,露馅了吧,原来是喝酒喝的啊.”“嘿嘿,让,让你发现了,今天来 ...

  3. C++设计模式实现--备忘录(Memento)模式

    一. 备忘录模式 定义:在不破坏封装性的前提下,捕获一个对象的内部状态.并在该对象之外保存这个状态. 这样以后就可将该对象恢复到原先保存的状态. 结构图: 使用范围: Memento 模式比較适用于功 ...

  4. 设计模式(十八)Memento模式

    在使用面向对象编程的方式实现撤销功能时,需要事先保存实例的相关状态信息.然后,在撤销时,还需要根据所保存的信息将实例恢复至原来的状态. 要想恢复实例,需要一个可以自由访问实例内部结构的权限.但是,如果 ...

  5. 备忘录(Memento)模式

    备忘录模式又叫做快照模式或者Token模式. 备忘录对象是一个用来存储另一个对象内部状态的快照的对象.备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉住,并外部化,存储起来,从而可以在将来 ...

  6. 《图解设计模式》读书笔记8-2 MEMENTO模式

    目录 Memento模式 示例代码 程序类图 代码 角色和类图 模式类图 角色 思路拓展 接口可见性 保存多少个Memento 划分Caretaker和Originator的意义 Memento模式 ...

  7. Java设计模式之从[暗黑破坏神存档点]分析备忘录(Memento)模式

    在大部分游戏中,都有一个"存档点"的概念.比如,在挑战boss前,游戏会在某个地方存档,假设玩家挑战boss失败,则会从这个存档点開始又一次游戏.因此,我们能够将这个"存 ...

  8. 设计模式C++描述----20.迭代器(Iterator)模式

    一. 举例说明 我们知道,在 STL 里提供 Iterator 来遍历 Vector 或者 List 数据结构. Iterator 模式也正是用来解决对一个聚合对象的遍历问题,将对聚合的遍历封装到一个 ...

  9. 设计模式的征途(C#实现)—文章目录索引

    1.预备篇 UML类图10分钟快速入门 2.创建型模式 ① 设计模式的征途-01.单例(Singleton)模式 ② 设计模式的征途-02.简单工厂(Simple Factory)模式 ③ 设计模式的 ...

随机推荐

  1. Java自学手记——接口

    抽象类 1.当类和对象被abstract修饰符修饰的时候,就变成抽象类或者抽象方法.抽象方法一定要在抽象类中,抽象类不能被创建对象,如果需要使用抽象类中的抽象方法,需要由子类重写抽象类中的方法,然后创 ...

  2. 互联网级监控系统必备-时序数据库之Influxdb集群及踩过的坑

    上篇博文中,我们介绍了做互联网级监控系统的必备-Influxdb的关键特性.数据读写.应用场景: 互联网级监控系统必备-时序数据库之Influxdb 本文中,我们介绍Influxdb数据库集群的搭建, ...

  3. Jenkins集成源码静态分析工具

    1.static code analysis插件说明 Jenkins提供了插件"static code analysis",该插件搜集不同的分析结果,并集合显示出来. 实际上,我们 ...

  4. PHP发送E-mail---新手教程

    首先下载PHPmailer拓展包,其实就是别人封装好的类库,下载链接:http://pan.baidu.com/s/1slbhGo1 首先去163注册个账号,然后登陆进去,点击设置下面的 POP3/S ...

  5. Spark源码阅读之存储体系--存储体系概述与shuffle服务

    一.概述 根据<深入理解Spark:核心思想与源码分析>一书,结合最新的spark源代码master分支进行源码阅读,对新版本的代码加上自己的一些理解,如有错误,希望指出. 1.块管理器B ...

  6. ubuntu(linux)新装系统恢复备份后无法进入系统问题的解决

    1. 引子: 可以跳过直接看3.~5. 我们可能会遇到这样的状况,从ubuntu转到别的系统,兜兜转转又回到了ubuntu. 比如说: Fedora升级后一大堆BUG,与java有关的软件渲染都糟糕的 ...

  7. Merge Two Sorted Lists & Remove Nth Node From End of List

    1.合并两个排好序的list Merge Two Sorted Lists Merge two sorted linked lists and return it as a new list. The ...

  8. python全栈阶段测试(一)

    1.执行Python脚本的两种方式 如果想要永久保存代码,就要用文件的方式 如果想要调试代码,就要用交互式的方式 2.Pyhton单行注释和多行注释分别用什么? 单行注释:# 多行注释: '' &qu ...

  9. (转)递归算法的时间复杂度终结篇与Master method

    开篇前言:为什么写这篇文章?笔者目前在学习各种各样的算法,在这个过程中,频繁地碰到到递归思想和分治思想,惊讶于这两种的思想的伟大与奇妙的同时,经常要面对的一个问题就是,对于一个给定的递归算法或者用分治 ...

  10. vue使用中的随笔

    在vue中vue-router配置的路径默认有"#"号,虽然无伤大雅,但是很多客户都不想看到,所以在初始配置路由的时候加上下面一句代码就可以了 mode:'history', 路径 ...