設計模式的征途—20.備忘錄(Memento)模式

Edison Chou發表於2017-08-23

相信每個人都有後悔的時候,但是人生並無後悔藥,有些錯誤一旦發生就無法再挽回,有些事一旦錯過就不會再重來,有些話一旦說出口也就不可能再收回,這就是人生。為了不讓自己後悔,我們總是需要三思而後行。這裡我們要學習一種可以在軟體中實現後悔機制的設計模式—備忘錄模式,它是軟體中的“後悔藥”。

備忘錄模式(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

  劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》

 

相關文章