设计模式的征途—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("", 1, 1);
        Display(chess);
        // 保存状态
        mc.Memento = chess.Save();
        chess.Y = 4;
        Display(chess);
        // 保存状态
        mc.Memento = chess.Save();
        Display(chess);
        chess.X = 5;
        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 = -1;
    private static NewMementoCaretaker mementoCaretaker = new NewMementoCaretaker();

    public static void Main()
    {
        Chessman chess = new Chessman("", 1, 1);
        Play(chess);
        chess.Y = 4;
        Play(chess);
        chess.X = 5;
        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 - 1));

        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 + 1));

        Console.WriteLine("棋子 {0} 当前位置为 第 {1} 行 第 {2} 列", chess.Label, chess.X, chess.Y);
    }

  编译运行后的结果如下图所示:

  

四、备忘录模式小结

4.1 主要优点

  (1)提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤。

  (2)实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。

4.2 主要缺点

  资源消耗过大,资源消耗过大,资源消耗过大 => 说三遍!因为每保存一次对象状态都需要消耗一定系统资源。

4.3 应用场景

  (1)需要保存一个对象在某一个时刻的全部状态或部分状态状态,以便需要在后面需要时可以恢复到先前的状态。

  (2)防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。

参考资料

  DesignPattern

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

 

posted @ 2017-08-23 23:51 Edison Chou 阅读(...) 评论(...) 编辑 收藏