設計模式:備忘錄模式 (Memento)

7 分鐘
軟體設計設計模式OOP

Memento 把物件的內部狀態擷取成一份快照。外部的管理者 (Caretaker) 可以儲存並還原這些快照,而不需要知道物件私有的實作細節。


意圖與用途

備忘錄模式把狀態打包成快照物件,外部的管理者 (Caretaker) 保管這份歷史,卻不需要知道裡面裝了什麼。

它與 Command 模式的差別在於:Command 記錄的是「操作」以及如何反轉操作;Memento 記錄的是「狀態快照」。在文字編輯器裡,兩者常常搭配使用。


結構與角色

  • Originator:產生備忘錄、也從備忘錄還原狀態的物件
  • Memento:某個特定時刻狀態的不可變快照
  • Caretaker:保管備忘錄歷史,但不去動它的內容

實作範例:文字編輯器還原功能

TypeScript
// Memento
class EditorMemento {
  constructor(
    private readonly content: string,
    private readonly cursorPosition: number,
    private readonly timestamp: Date = new Date(),
  ) {}

  getContent(): string { return this.content; }
  getCursorPosition(): number { return this.cursorPosition; }
  getTimestamp(): Date { return this.timestamp; }
}

// Originator
class TextEditor {
  private content = '';
  private cursorPosition = 0;

  type(text: string): void {
    this.content =
      this.content.slice(0, this.cursorPosition) +
      text +
      this.content.slice(this.cursorPosition);
    this.cursorPosition += text.length;
  }

  moveCursor(position: number): void {
    this.cursorPosition = Math.max(0, Math.min(position, this.content.length));
  }

  getContent(): string { return this.content; }
  getCursor(): number { return this.cursorPosition; }

  // 建立備忘錄
  save(): EditorMemento {
    return new EditorMemento(this.content, this.cursorPosition);
  }

  // 從備忘錄還原
  restore(memento: EditorMemento): void {
    this.content = memento.getContent();
    this.cursorPosition = memento.getCursorPosition();
  }
}

// Caretaker
class EditorHistory {
  private mementos: EditorMemento[] = [];

  save(memento: EditorMemento): void {
    this.mementos.push(memento);
  }

  undo(): EditorMemento | undefined {
    return this.mementos.pop();
  }

  getCount(): number { return this.mementos.length; }
}

// 使用
const editor = new TextEditor();
const history = new EditorHistory();

history.save(editor.save());
editor.type('Hello');

history.save(editor.save());
editor.type(', World');

history.save(editor.save());
editor.type('!');

console.log(editor.getContent()); // 'Hello, World!'

editor.restore(history.undo()!);
console.log(editor.getContent()); // 'Hello, World'

editor.restore(history.undo()!);
console.log(editor.getContent()); // 'Hello'

適用情境

適用時機

  • 需要撤銷/重做,或定期建立快照
  • 不希望把物件的內部狀態暴露給外部的儲存機制

Memento vs. Command

Command 記錄的是「發生了什麼」以及如何反轉;Memento 記錄的是「某個時間點的狀態」。文字編輯器常常兩者並用:Command 追蹤操作,Memento 在每次變更前拍下快照。


總結

Memento 的精髓:在不暴露物件內部細節的前提下,儲存與還原狀態。草稿狀態、還原點,以及離開頁面前的自動儲存,都是 Memento 最自然的應用場景。