Memento Pattern(備忘錄模式)

是什麼?

Memento Pattern 讓你在不破壞封裝性的前提下,捕獲物件的內部狀態並存放在外部,之後可以恢復到該狀態。狀態的儲存和恢復不會暴露物件的內部實作細節。

ℹ️GoF 分類

Memento 屬於行為型模式(Behavioral Pattern),重點在於安全地保存和恢復物件狀態。

什麼時候用?

什麼時候不該用?

⚠️過度設計警告

如果物件的狀態很大(例如包含大量資料或檔案參考),頻繁建立 Memento 會消耗大量記憶體。此時考慮只儲存差異(Delta)而非完整快照。如果狀態只有一兩個欄位,直接存變數就好,不需要 Memento。

執行流程

1

定義 Memento

封裝 Originator 的內部狀態快照

2

定義 Originator

建立 Memento 和從 Memento 恢復狀態

3

定義 Caretaker

負責保管 Memento,但不能查看或修改其內容

4

保存狀態

Originator 建立 Memento,Caretaker 存起來

5

恢復狀態

Caretaker 取出 Memento,Originator 從中恢復

流程解讀:Memento 的核心是「封裝性」。Memento 是一個不可變的黑盒子(Immutable,指物件建立後其內部狀態不能被修改),只有 Originator 知道怎麼讀寫它的內容。Caretaker 用 Stack 或 List 保管多個 Memento,實現多層 Undo。恢復時從 Stack 中 Pop 出最近的 Memento,交給 Originator 還原。

程式碼範例

C# 版本

csharp
// 1. Memento:狀態快照(不可變)
public record GameMemento(int Hp, int Mp, string Location, DateTime SavedAt);
 
// 2. Originator:遊戲角色
public class GameCharacter
{
    public int Hp { get; private set; }
    public int Mp { get; private set; }
    public string Location { get; private set; }
 
    public GameCharacter(int hp, int mp, string location)
    {
        Hp = hp; Mp = mp; Location = location;
    }
 
    public GameMemento Save()
    {
        Console.WriteLine($"  [存檔] HP:{Hp} MP:{Mp} 位置:{Location}");
        return new GameMemento(Hp, Mp, Location, DateTime.Now);
    }
 
    public void Restore(GameMemento memento)
    {
        Hp = memento.Hp;
        Mp = memento.Mp;
        Location = memento.Location;
        Console.WriteLine($"  [讀檔] HP:{Hp} MP:{Mp} 位置:{Location}");
    }
 
    public void TakeDamage(int damage)
    {
        Hp = Math.Max(0, Hp - damage);
        Console.WriteLine($"  受到 {damage} 傷害,HP 剩餘 {Hp}");
    }
 
    public void MoveTo(string location)
    {
        Location = location;
        Console.WriteLine($"  移動到 {location}");
    }
}
 
// 3. Caretaker:存檔管理器
public class SaveManager
{
    private readonly Stack<GameMemento> _saves = new();
 
    public void Save(GameMemento memento) => _saves.Push(memento);
 
    public GameMemento? Load()
    {
        return _saves.Count > 0 ? _saves.Pop() : null;
    }
 
    public int SaveCount => _saves.Count;
}
 
// 4. 使用
var hero = new GameCharacter(100, 50, "村莊");
var saveManager = new SaveManager();
 
saveManager.Save(hero.Save());       // 存檔 1
hero.MoveTo("Boss 房間");
saveManager.Save(hero.Save());       // 存檔 2
hero.TakeDamage(80);                 // HP 剩 20
hero.TakeDamage(30);                 // HP 歸零
 
Console.WriteLine("\n打輸了!讀取上一個存檔...");
var lastSave = saveManager.Load();
if (lastSave != null) hero.Restore(lastSave); // 回到 Boss 房間,HP 100

TypeScript 版本

typescript
// 1. Memento
class GameMemento {
  constructor(
    public readonly hp: number,
    public readonly mp: number,
    public readonly location: string,
    public readonly savedAt: Date = new Date()
  ) {}
}
 
// 2. Originator
class GameCharacter {
  constructor(
    public hp: number,
    public mp: number,
    public location: string
  ) {}
 
  save(): GameMemento {
    console.log(`  [存檔] HP:${this.hp} MP:${this.mp} 位置:${this.location}`);
    return new GameMemento(this.hp, this.mp, this.location);
  }
 
  restore(memento: GameMemento) {
    this.hp = memento.hp;
    this.mp = memento.mp;
    this.location = memento.location;
    console.log(`  [讀檔] HP:${this.hp} MP:${this.mp} 位置:${this.location}`);
  }
 
  takeDamage(damage: number) {
    this.hp = Math.max(0, this.hp - damage);
    console.log(`  受到 ${damage} 傷害,HP 剩餘 ${this.hp}`);
  }
 
  moveTo(location: string) {
    this.location = location;
    console.log(`  移動到 ${location}`);
  }
}
 
// 3. Caretaker
class SaveManager {
  private saves: GameMemento[] = [];
 
  save(memento: GameMemento) { this.saves.push(memento); }
  load(): GameMemento | undefined { return this.saves.pop(); }
}
 
// 4. 使用
const hero = new GameCharacter(100, 50, "村莊");
const saveManager = new SaveManager();
 
saveManager.save(hero.save());
hero.moveTo("Boss 房間");
saveManager.save(hero.save());
hero.takeDamage(80);
hero.takeDamage(30);
 
console.log("\n打輸了!讀取上一個存檔...");
const lastSave = saveManager.load();
if (lastSave) hero.restore(lastSave);

Python 版本

python
from dataclasses import dataclass
from datetime import datetime
 
# 1. Memento
@dataclass(frozen=True)
class GameMemento:
    hp: int
    mp: int
    location: str
    saved_at: datetime = datetime.now()
 
# 2. Originator
class GameCharacter:
    def __init__(self, hp: int, mp: int, location: str):
        self.hp = hp
        self.mp = mp
        self.location = location
 
    def save(self) -> GameMemento:
        print(f"  [存檔] HP:{self.hp} MP:{self.mp} 位置:{self.location}")
        return GameMemento(self.hp, self.mp, self.location)
 
    def restore(self, memento: GameMemento) -> None:
        self.hp = memento.hp
        self.mp = memento.mp
        self.location = memento.location
        print(f"  [讀檔] HP:{self.hp} MP:{self.mp} 位置:{self.location}")
 
    def take_damage(self, damage: int) -> None:
        self.hp = max(0, self.hp - damage)
        print(f"  受到 {damage} 傷害,HP 剩餘 {self.hp}")
 
    def move_to(self, location: str) -> None:
        self.location = location
        print(f"  移動到 {location}")
 
# 3. Caretaker
class SaveManager:
    def __init__(self):
        self._saves: list[GameMemento] = []
 
    def save(self, memento: GameMemento) -> None:
        self._saves.append(memento)
 
    def load(self) -> GameMemento | None:
        return self._saves.pop() if self._saves else None
 
# 4. 使用
hero = GameCharacter(100, 50, "村莊")
save_manager = SaveManager()
 
save_manager.save(hero.save())
hero.move_to("Boss 房間")
save_manager.save(hero.save())
hero.take_damage(80)
hero.take_damage(30)
 
print("\n打輸了!讀取上一個存檔...")
last_save = save_manager.load()
if last_save:
    hero.restore(last_save)

Java 版本

java
import java.time.LocalDateTime;
import java.util.Stack;
 
// 1. Memento(不可變)
public record GameMemento(int hp, int mp, String location, LocalDateTime savedAt) {
    public GameMemento(int hp, int mp, String location) {
        this(hp, mp, location, LocalDateTime.now());
    }
}
 
// 2. Originator
public class GameCharacter {
    private int hp;
    private int mp;
    private String location;
 
    public GameCharacter(int hp, int mp, String location) {
        this.hp = hp; this.mp = mp; this.location = location;
    }
 
    public GameMemento save() {
        System.out.printf("  [存檔] HP:%d MP:%d 位置:%s%n", hp, mp, location);
        return new GameMemento(hp, mp, location);
    }
 
    public void restore(GameMemento m) {
        hp = m.hp(); mp = m.mp(); location = m.location();
        System.out.printf("  [讀檔] HP:%d MP:%d 位置:%s%n", hp, mp, location);
    }
 
    public void takeDamage(int damage) {
        hp = Math.max(0, hp - damage);
        System.out.println("  受到 " + damage + " 傷害,HP 剩餘 " + hp);
    }
 
    public void moveTo(String loc) {
        location = loc;
        System.out.println("  移動到 " + loc);
    }
}
 
// 3. Caretaker
public class SaveManager {
    private final Stack<GameMemento> saves = new Stack<>();
 
    public void save(GameMemento m) { saves.push(m); }
    public GameMemento load() { return saves.isEmpty() ? null : saves.pop(); }
}
 
// 4. 使用
GameCharacter hero = new GameCharacter(100, 50, "村莊");
SaveManager saveManager = new SaveManager();
 
saveManager.save(hero.save());
hero.moveTo("Boss 房間");
saveManager.save(hero.save());
hero.takeDamage(80);
hero.takeDamage(30);
 
System.out.println("\n打輸了!讀取上一個存檔...");
GameMemento lastSave = saveManager.load();
if (lastSave != null) hero.restore(lastSave);

結構圖

Client
requests save/load
Caretaker (SaveManager)
stores
Originator (GameCharacter)
creates / restores from
Memento (GameMemento)

結構解讀:三個角色各有嚴格的權限分工。Originator 是唯一能「建立」和「讀取」Memento 內容的角色。Caretaker 只負責「保管」Memento,不能查看或修改其內部(就像銀行保管箱,銀行保管但不能打開)。Client 透過 Caretaker 請求存檔/讀檔,但實際的狀態捕獲和恢復由 Originator 完成。這種設計確保了封裝性不被破壞。

實戰補充

💡資深開發者經驗

Undo 系統:文字編輯器、繪圖軟體的 Undo 通常是 Command Pattern + Memento Pattern 的組合。Command 記錄「做了什麼」,Memento 記錄「之前是什麼狀態」。

交易回滾:資料庫的 Transaction 就是 Memento 的概念——ROLLBACK 讓資料回到 BEGIN TRANSACTION 時的狀態。應用層也可以用 Memento 實作類似的機制。

記憶體管理:大型物件頻繁存快照會吃掉大量記憶體。實務上常用以下策略:限制 Memento 數量(只保留最近 N 個)、只儲存變更的部分(Delta Encoding)、或將 Memento 序列化到磁碟。

不可變 Memento:Memento 必須是不可變的(Immutable)。如果 Memento 被外部修改,恢復時就會得到錯誤的狀態。C# 的 record、Java 的 record、Python 的 frozen dataclass 都很適合。

理解測驗

🤔 Memento Pattern 中,誰有權存取 Memento 的內部資料?

🤔 使用 Memento Pattern 最常見的問題是什麼?

🤔 Memento Pattern 和 Command Pattern 的 Undo 機制有什麼差異?

面試常見問題

Q: 如何處理 Memento 的記憶體問題?

A: 四種策略:(1) 限制數量 — 只保留最近 N 個快照(如 Undo 最多 50 步),超過就丟棄最舊的。(2) Delta Encoding — 只儲存與上一個快照的差異而非完整狀態。(3) 序列化到磁碟 — 把 Memento 序列化成 JSON/Binary 存到檔案系統。(4) 壓縮 — 對序列化後的資料做壓縮。選擇取決於場景:編輯器用限制數量,版本控制用 Delta,長期保存用序列化。

Q: 在多執行緒環境下,Memento 有什麼問題?

A: 如果 Originator 的狀態在 Save 過程中被其他執行緒修改,快照可能不一致。解法:(1) Save 時加鎖(lock/synchronized)。(2) 用不可變資料結構(Immutable Collections),Copy-on-Write 自然保證一致性。(3) 在 Save 時做 deep copy 而非引用。

相關模式

| 模式 | 關係 | |------|------| | Command | 搭配使用 — Command 記錄「做了什麼操作」(用於反向 Undo),Memento 記錄「之前的完整狀態」(用於快照 Undo) | | Iterator | Iterator 的走訪位置可以用 Memento 保存為「書籤」,稍後從該位置繼續走訪 | | Prototype | 都涉及物件狀態的複製,但 Prototype 的目的是建立新物件,Memento 的目的是回復舊狀態 | | State | State 管理物件的「當前行為」,Memento 保存物件的「歷史快照」。可搭配使用實現狀態機的回滾 |

重點整理

💡一句話記住

Memento Pattern = 遊戲存檔:拍快照不破壞封裝,讀檔回到任何時間點。 口訣:「存檔不偷看,讀檔不破壞」

| 概念 | 說明 | |------|------| | Memento(備忘錄) | 儲存 Originator 某一時刻的狀態快照 | | Originator(原始者) | 建立 Memento 和從 Memento 恢復狀態 | | Caretaker(管理者) | 保管 Memento,但不能查看或修改其內容 | | 核心好處 | 不破壞封裝的前提下保存和恢復狀態 | | 代價 | 完整快照消耗記憶體,需要管理快照數量 |

你可能也想看

Visitor PatternInterpreter Pattern

按 ← → 鍵切換課程