1. 程式人生 > >設計模式的征途—20.備忘錄(Memento)模式

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

行為 修煉之道 mda 3.2 ima 位置 pri 捕獲 spl

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

備忘錄模式(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)防止外界對象破壞一個對象歷史狀態的封裝性,避免將對象歷史狀態的實現細節暴露給外界對象。

參考資料

  技術分享

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

作者:周旭龍

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

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。

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