引子:如果再給我一次機會#
打單機 PC 遊戲時,玩家通常在打大 Boss 前先存進度,萬一通關失敗就讀回剛才的存檔重新挑戰。
軟體比人生美妙——時間不能倒流,但軟體狀態可以恢復。
一些更頻繁的應用:下棋悔棋、編輯器撤銷、瀏覽器後退……都不必存到磁碟,只需在記憶體中保存一份即可恢復。
第一版:直接保存實例#
class GameRole
{
public int Vitality { get; set; }
public int Attack { get; set; }
public int Defense { get; set; }
public void StateDisplay() { /* 顯示三項數值 */ }
public void GetInitState() { Vitality = 100; Attack = 100; Defense = 100; }
public void Fight() { Vitality = 0; Attack = 0; Defense = 0; }
}客戶端:
GameRole lixiaoyao = new GameRole();
lixiaoyao.GetInitState();
// 保存進度
GameRole backup = new GameRole();
backup.Vitality = lixiaoyao.Vitality;
backup.Attack = lixiaoyao.Attack;
backup.Defense = lixiaoyao.Defense;
lixiaoyao.Fight(); // 大戰 Boss,三項全歸零
// 恢復進度
lixiaoyao.Vitality = backup.Vitality;
lixiaoyao.Attack = backup.Attack;
lixiaoyao.Defense = backup.Defense;客戶端被迫知道遊戲角色的全部細節:生命力、攻擊力、防禦力都要逐一備份。
- 如果新增「魔法力」屬性 → 客戶端要改
- 如果把「生命力」改成「經驗值」 → 客戶端要改
違反封裝與單一職責原則。
備忘錄模式#
備忘錄模式(Memento Pattern):在不破壞封裝性的前提下,捕獲一個物件的內部狀態,並在該物件之外保存這個狀態。這樣以後就可將該物件恢復到原先保存的狀態。[DP]
結構#
- Originator(發起人):負責創建備忘錄,記錄當前時刻的內部狀態,並可使用備忘錄恢復內部狀態。可決定 Memento 存儲哪些內部狀態
- Memento(備忘錄):負責存儲 Originator 物件的內部狀態,並防止 Originator 以外的物件訪問備忘錄
- Caretaker(管理者):負責保存備忘錄,但不能對備忘錄的內容進行操作或檢查
classDiagram
class Originator {
-State
+CreateMemento() Memento
+SetMemento(Memento)
+Show()
}
class Memento {
-State
}
class Caretaker {
-Memento memento
}
Originator ..> Memento : creates
Caretaker o--> Memento雙重介面#
備忘錄有兩個介面:
- Caretaker 只能看到備忘錄的窄介面——只能傳遞給其他物件
- Originator 能看到寬介面——允許訪問返回到先前狀態所需的所有資料
通用結構#
class Originator
{
public string State { get; set; }
public Memento CreateMemento() => new Memento(State);
public void SetMemento(Memento memento) => State = memento.State;
public void Show() => Console.WriteLine("State=" + State);
}
class Memento
{
public string State { get; }
public Memento(string state) { State = state; }
}
class Caretaker
{
public Memento Memento { get; set; }
}客戶端:
Originator o = new Originator();
o.State = "On";
Caretaker c = new Caretaker();
c.Memento = o.CreateMemento();
o.State = "Off";
o.SetMemento(c.Memento);第二版:用備忘錄改寫遊戲存檔#
class RoleStateMemento
{
public int Vitality { get; set; }
public int Attack { get; set; }
public int Defense { get; set; }
public RoleStateMemento(int vit, int atk, int def)
{
Vitality = vit; Attack = atk; Defense = def;
}
}
class RoleStateCaretaker
{
public RoleStateMemento Memento { get; set; }
}
class GameRole
{
// 屬性同前
public RoleStateMemento SaveState() => new RoleStateMemento(Vitality, Attack, Defense);
public void RecoveryState(RoleStateMemento memento)
{
Vitality = memento.Vitality;
Attack = memento.Attack;
Defense = memento.Defense;
}
}客戶端:
GameRole lixiaoyao = new GameRole();
lixiaoyao.GetInitState();
RoleStateCaretaker stateAdmin = new RoleStateCaretaker();
stateAdmin.Memento = lixiaoyao.SaveState();
lixiaoyao.Fight();
lixiaoyao.RecoveryState(stateAdmin.Memento);客戶端不需要知道存了什麼——所有細節封裝在
RoleStateMemento中。之後修改保存內容(新增屬性等)只影響 Originator 與 Memento 兩個類,客戶端不變。
何時使用?#
- 功能比較複雜、但需要維護或記錄屬性歷史的類
- 需要保存的屬性只是眾多屬性中的一小部分時,Originator 可只將需要的部分存入 Memento
- 角色狀態改變時可能無效,需要回到先前狀態
- 配合命令模式(第 23 章)實現命令的撤銷功能
缺點:如果狀態數據很大很多,備忘錄物件會非常耗記憶體。
如同存檔太多會占滿磁碟——備忘錄不是越多越好。