引子:如果再給我一次機會#

打單機 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 章)實現命令的撤銷功能

缺點:如果狀態數據很大很多,備忘錄物件會非常耗記憶體

如同存檔太多會占滿磁碟——備忘錄不是越多越好。