# 19. Memento
###### tags: `DesignPatterns`
## 關於 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](https://en.wikipedia.org/wiki/Design_Patterns)
- 不違反封裝的情況下,取得物件的內部狀態,以便之後可以將物件還原到先前狀態
Memento(備忘錄)屬於行為型(Behavioral Patterns),當遇到需要**狀態回到某個狀態**時,可藉由 Memento 來將物件備份,包含物件中 private 的屬性都可以透過 Memento 來記錄,也可以將每次異動物件都記錄至 Memento 來達到上一步、下一步的功能。
優點:
- 將儲存狀態的邏輯放在 Memento 使原本物件職責維持單一
缺點:
- 過多的紀錄會大量消耗記憶體
---
## 2. UML

Class 間關聯:
- Originator 依賴 Memento
- Caretaker 可包含 Memento
Class:
- Originator:內部狀態需要保存的物件
- Memento:負責保存 Originator 的狀態
- Caretaker:存放 Memento 但不操作
---
## 3. 將 UML 轉為程式碼
內部狀態需要保存的物件
```C#
/// <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 的狀態
```C#
/// <summary>
/// 負責保存 Originator 的狀態
/// </summary>
public class Memento
{
public string State { get; }
public Memento(string state)
{
State = state;
}
}
```
存放 Memento 但不操作
```C#
/// <summary>
/// 存放 Memento 但不操作
/// </summary>
public class Caretaker
{
public Memento Memento { set; get; }
}
```
1. 建立`originator`並給定初始值 On
2. 建立`caretaker`並透過`originator`建立`Memento`
3. 改變`State`的值
4. 透過`Memento`還原`State`的值
```C#
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();
}
```
執行結果
```Console
State = On
State = Off
Restoring state...
State = On
```
---
## 4. 情境
我們接到了一個門市結帳刷條碼要能夠「上一步」的需求
- 每次新增商品會增加一份商品快照
- 點選「上一步」時會還原至上一份快照
結帳刷條碼
```C#
/// <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>`保存每次刷條碼新增商品的快照
```C#
/// <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 但不操作
```C#
/// <summary>
/// 存放 Memento 但不操作
/// </summary>
public class Caretaker
{
public Memento Memento { set; get; }
}
```
1. 建立`scanTheBarcode`
2. 建立`caretaker`並透過`scanTheBarcode`建立`Memento`
3. 新增商品
4. 透過`PreviousStep`移除前一項商品
```C#
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();
}
```
執行結果
```Console
Products = 麵包
Products = 麵包, 蘋果
Products = 麵包, 蘋果, 餅乾
Products = 麵包, 蘋果, 餅乾, 蛋糕切片
== 上一步 ==
Products = 麵包, 蘋果, 餅乾
Products = 麵包, 蘋果, 餅乾, 整塊蛋糕
```
---
## 完整程式碼
GitHub:[Behavioral_06_Memento](https://github.com/darionnnnnn/blog/tree/master/Blog/Behavioral_06_Memento)
---
## 總結
### Memento 若是每次都將整個物件備份會大量消耗記憶體,而折衷的辦法可以在較短的時間內將每次物件異動的差異備份下來,每隔一段時間再將整個物件備份,來達到節省記憶體的目的,而時間長短則依據需求而定。
---
## 參考資料
1. [Design Patterns](https://en.wikipedia.org/wiki/Design_Patterns)
2. [大話設計模式](https://www.tenlong.com.tw/products/9789866761799)
2. [dofactory](https://www.dofactory.com/)
3. [Refactoring.Guru](https://refactoring.guru/)
---
## 新手上路,若有錯誤還請告知,謝謝