19. Memento


Posted by WayneCheng on 2021-01-26

關於 Memento 本篇將討論以下幾個問題

1. 關於 Memento

2. UML

3. 將 UML 轉為程式碼

4. 情境


測試環境:

OS:Windows 10
IDE:Visual Studio 2019


1. 關於 Memento

Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.

by Gang of Four

  • 不違反封裝的情況下,取得物件的內部狀態,以便之後可以將物件還原到先前狀態

Memento(備忘錄)屬於行為型(Behavioral Patterns),當遇到需要狀態回到某個狀態時,可藉由 Memento 來將物件備份,包含物件中 private 的屬性都可以透過 Memento 來記錄,也可以將每次異動物件都記錄至 Memento 來達到上一步、下一步的功能。

優點:

  • 將儲存狀態的邏輯放在 Memento 使原本物件職責維持單一

缺點:

  • 過多的紀錄會大量消耗記憶體

2. UML

Class 間關聯:

  • Originator 依賴 Memento
  • Caretaker 可包含 Memento

Class:

  • Originator:內部狀態需要保存的物件
  • Memento:負責保存 Originator 的狀態
  • Caretaker:存放 Memento 但不操作

3. 將 UML 轉為程式碼

內部狀態需要保存的物件

/// <summary>
/// 內部狀態需要保存的物件
/// </summary>
public class Originator
{
    private string _state;

    public string State
    {
        get => _state;
        set
        {
            _state = value;
            Console.WriteLine($"State = {_state}");
        }
    }

    public Memento CreateMemento()
    {
        return (new Memento(_state));
    }

    public void SetMemento(Memento memento)
    {
        Console.WriteLine("Restoring state...");
        State = memento.State;
    }
}

負責保存 Originator 的狀態

/// <summary>
/// 負責保存 Originator 的狀態
/// </summary>
public class Memento
{
    public string State { get; }

    public Memento(string state)
    {
        State = state;
    }
}

存放 Memento 但不操作

/// <summary>
/// 存放 Memento 但不操作
/// </summary>
public class Caretaker
{
    public Memento Memento { set; get; }
}
  1. 建立originator並給定初始值 On
  2. 建立caretaker並透過originator建立Memento
  3. 改變State的值
  4. 透過Memento還原State的值
static void Main(string[] args)
{
    Default.Originator originator = new Default.Originator { State = "On" };
    Default.Caretaker caretaker = new Default.Caretaker
    {
        Memento = originator.CreateMemento()
    };

    originator.State = "Off";

    originator.SetMemento(caretaker.Memento);
    Console.ReadLine();
}

執行結果

State = On
State = Off
Restoring state...
State = On

4. 情境

我們接到了一個門市結帳刷條碼要能夠「上一步」的需求

  • 每次新增商品會增加一份商品快照
  • 點選「上一步」時會還原至上一份快照

結帳刷條碼

/// <summary>
/// 結帳刷條碼
/// </summary>
public class ScanTheBarcode
{
    private string _products;

    public void AddProducts(Memento memento, string product)
    {
        if (string.IsNullOrWhiteSpace(_products))
        {
            _products = product;
        }
        else
        {
            _products += ", " + product;
        }

        memento.SetState(_products);

        Console.WriteLine($"Products = {_products}");
    }

    public Memento CreateMemento()
    {
        return (new Memento());
    }

    public void PreviousStep(Memento memento)
    {
        Console.WriteLine("\n== 上一步 ==");
        memento.State.Pop();
        _products = memento.State.Pop();
        Console.WriteLine($"Products = {_products}");
    }
}

使用Stack<string>保存每次刷條碼新增商品的快照

/// <summary>
/// 負責保存 ScanTheBarcode 的狀態
/// </summary>
public class Memento
{
    public Stack<string> State { get; }

    public Memento()
    {
        State = new Stack<string>();
    }

    public void SetState(string state)
    {
        State.Push(state);
    }
}

存放 Memento 但不操作

/// <summary>
/// 存放 Memento 但不操作
/// </summary>
public class Caretaker
{
    public Memento Memento { set; get; }
}
  1. 建立scanTheBarcode
  2. 建立caretaker並透過scanTheBarcode建立Memento
  3. 新增商品
  4. 透過PreviousStep移除前一項商品
static void Main(string[] args)
{
    Situation.ScanTheBarcode scanTheBarcode = new Situation.ScanTheBarcode();
    Situation.Caretaker caretaker = new Situation.Caretaker
    {
        Memento = scanTheBarcode.CreateMemento()
    };

    scanTheBarcode.AddProducts(caretaker.Memento, "麵包");
    scanTheBarcode.AddProducts(caretaker.Memento, "蘋果");
    scanTheBarcode.AddProducts(caretaker.Memento, "餅乾");
    scanTheBarcode.AddProducts(caretaker.Memento, "蛋糕切片");
    scanTheBarcode.PreviousStep(caretaker.Memento);
    scanTheBarcode.AddProducts(caretaker.Memento, "整塊蛋糕");

    Console.ReadLine();
}

執行結果

Products = 麵包
Products = 麵包, 蘋果
Products = 麵包, 蘋果, 餅乾
Products = 麵包, 蘋果, 餅乾, 蛋糕切片

== 上一步 ==
Products = 麵包, 蘋果, 餅乾
Products = 麵包, 蘋果, 餅乾, 整塊蛋糕

完整程式碼

GitHub:Behavioral_06_Memento


總結

Memento 若是每次都將整個物件備份會大量消耗記憶體,而折衷的辦法可以在較短的時間內將每次物件異動的差異備份下來,每隔一段時間再將整個物件備份,來達到節省記憶體的目的,而時間長短則依據需求而定。


參考資料

  1. Design Patterns
  2. 大話設計模式
  3. dofactory
  4. Refactoring.Guru

新手上路,若有錯誤還請告知,謝謝


#designpattern #CSharp







Related Posts

清晰說明針孔相機的內部參數與外部參數矩陣

清晰說明針孔相機的內部參數與外部參數矩陣

ClearDB id 連續遞增問題

ClearDB id 連續遞增問題

關於 物件 Object - 物件建構式

關於 物件 Object - 物件建構式


Comments