Published on

备忘录模式详解

Authors
  • avatar
    Name
    青雲
    Twitter

备忘录模式(Memento Pattern)是一种行为型设计模式,允许在不破坏封装性的前提下,捕获对象的内部状态,并在将来需要时恢复到原先的状态。主要应用于支持撤销和恢复操作的应用程序中,例如文本编辑器、游戏存档系统等。

为什么需要备忘录模式?

在软件开发中,有时需要记录对象的状态,以便在需要时恢复。直接存储对象的引用或深拷贝对象会破坏其封装性,难以维护。通过备忘录模式,可以存取对象的历史状态,而对象本身无需暴露其内部结构和数据。

基本概念

备忘录模式主要包含以下几个角色:

  1. 发起人(Originator):创建一个包含其当前内部状态的备忘录,并使用备忘录恢复内部状态。
  2. 备忘录(Memento):存储发起人的内部状态,并防止其他对象访问备忘录。
  3. 负责人(Caretaker):负责保存备忘录,但不能修改和访问备忘录的内容,只负责传递备忘录给其他对象。

实现示例

假设我们要实现一个文本编辑器的撤销和恢复功能,可以使用备忘录模式保存和恢复文本编辑器的状态。

定义备忘录类

// 备忘录类,存储发起人的内部状态
class Memento {
  private readonly state: string;

  constructor(state: string) {
    this.state = state;
  }

  getState(): string {
    return this.state;
  }
}

定义发起人类

// 发起人类,创建和使用备忘录
class TextEditor {
  private text: string = '';

  setText(text: string): void {
    this.text = text;
  }

  getText(): string {
    return this.text;
  }

  // 创建备忘录,保存当前状态
  createMemento(): Memento {
    return new Memento(this.text);
  }

  // 使用备忘录恢复状态
  restoreMemento(memento: Memento): void {
    this.text = memento.getState();
  }
}

定义负责人类

// 负责人类,保存和管理备忘录
class TextEditorHistory {
  private mementos: Memento[] = [];

  saveMemento(memento: Memento): void {
    this.mementos.push(memento);
  }

  getMemento(index: number): Memento | undefined {
    return this.mementos[index];
  }
}

使用备忘录模式实现撤销和恢复功能

const editor = new TextEditor();
const history = new TextEditorHistory();

editor.setText("Hello");
history.saveMemento(editor.createMemento());

editor.setText("Hello, World");
history.saveMemento(editor.createMemento());

editor.setText("Hello, World!!!");

console.log("Current Text:", editor.getText()); // 输出: Current Text: Hello, World!!!

// 撤销操作
const memento1 = history.getMemento(1);
if (memento1) {
  editor.restoreMemento(memento1);
}
console.log("After undo:", editor.getText()); // 输出: After undo: Hello, World

// 再次撤销操作
const memento0 = history.getMemento(0);
if (memento0) {
  editor.restoreMemento(memento0);
}
console.log("After second undo:", editor.getText()); // 输出: After second undo: Hello

应用场景

备忘录模式在前端开发中有着广泛的应用场景,特别是在需要管理和恢复状态的情况下。通过使用备忘录模式自动保存和恢复用户输入的数据,可以显著提高用户体验。以下是一些典型的应用场景和案例。

表单数据的自动保存和恢复

在填写长表单或复杂表单时,为了防止数据丢失(如由于浏览器崩溃、意外关闭等),可以使用备忘录模式自动保存用户的输入。如果用户离开并重新访问表单页面,可以提示用户恢复上次会话的状态。

class FormState {
  private state: { [key: string]: any } = {};

  saveState(): string {
    return JSON.stringify(this.state);
  }

  restoreState(memento: string): void {
    this.state = JSON.parse(memento);
  }

  updateState(newState: { [key: string]: any }): void {
    this.state = { ...this.state, ...newState };
  }

  getState(): { [key: string]: any } {
    return this.state;
  }
}

// 备忘录管理者
class FormStateManager {
  private savedStates: string[] = [];

  add(state: string): void {
    this.savedStates.push(state);
  }

  getLatest(): string | undefined {
    return this.savedStates[this.savedStates.length - 1];
  }
}

// 使用示例
const formState = new FormState();
const formStateManager = new FormStateManager();

formState.updateState({ name: 'John', email: '[email protected]' });
formStateManager.add(formState.saveState());

formState.updateState({ name: 'John Doe' });

console.log('Current State:', formState.getState()); // 输出: Current State: { name: "John Doe", email: "[email protected]" }

const latestState = formStateManager.getLatest();
if (latestState) {
  formState.restoreState(latestState);
}

console.log('Restored State:', formState.getState()); // 输出: Restored State: { name: "John", email: "[email protected]" }

富文本编辑器的撤销/重做功能

富文本编辑器中的撤销和重做功能是备忘录模式的经典应用。通过保存每次编辑操作的状态(如插入文字、设置格式等操作),用户可以撤销到任意一步之前的状态,或者从撤销的地方重做。

class EditorState {
  private content: string;

  constructor(content: string) {
    this.content = content;
  }

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

// 备忘录管理者
class History {
  private states: EditorState[] = [];
  private current: number = -1; // 指向当前状态

  saveState(state: EditorState): void {
    // 每次保存新状态时,删除当前状态之后的所有状态
    this.states = this.states.slice(0, this.current + 1);
    this.states.push(state);
    this.current++;
  }

  undo(): EditorState | null {
    if (this.current > 0) {
      this.current--;
      return this.states[this.current];
    }
    return null;
  }

  redo(): EditorState | null {
    if (this.current < this.states.length - 1) {
      this.current++;
      return this.states[this.current];
    }
    return null;
  }
}

// 使用示例
const editorHistory = new History();
const editor = new EditorState('');

editorHistory.saveState(new EditorState('Hello'));
editorHistory.saveState(new EditorState('Hello, World'));
editorHistory.saveState(new EditorState('Hello, World!!!'));

console.log('Current Content:', editor.getContent()); // 输出: Current Content: Hello, World!!!

const undoState1 = editorHistory.undo();
if (undoState1) {
  editor = undoState1;
}
console.log('After undo:', editor.getContent()); // 输出: After undo: Hello, World

const undoState0 = editorHistory.undo();
if (undoState0) {
  editor = undoState0;
}
console.log('After second undo:', editor.getContent()); // 输出: After second undo: Hello

const redoState1 = editorHistory.redo();
if (redoState1) {
  editor = redoState1;
}
console.log('After redo:', editor.getContent()); // 输出: After redo: Hello, World

与命令模式对比

《命令模式详解》一文中提到,命令模式也会应用于文本编辑器的撤销/重做功能。但它们的工作方式和应用场景有所不同。 备忘录模式:

  • 状态存储:存储对象的整体状态,适用于需要保存和恢复对象整体状态的场景。
  • 实现简单:通过直接存储对象的状态来实现,但可能会占用较多内存。
  • 封装性较强:不需要暴露对象的内部结构和数据。

命令模式:

  • 命令存储:存储每个操作(命令)的历史记录,适用于记录操作步骤并能灵活撤销和重做的场景。
  • 实现灵活:可以记录操作过程,支持复杂的操作恢复和重做逻辑。
  • 状态可控:可以更细粒度地控制对象的操作和状态变化。

总结来说,备忘录模式和命令模式各有其独特的应用场景和优势。在实际开发中,选择哪种模式取决于具体的需求以及系统的复杂性。如果需要保存和恢复对象整体状态,备忘录模式是一个不错的选择;如果需要记录操作步骤并支持复杂的撤销和重做操作,命令模式会更加灵活和合适。

游戏的进度保存和加载

在游戏应用中,可以使用备忘录模式保存玩家的当前游戏进度,如关卡、得分、物品等。如果玩家失败了或者想重新开始,可以从最近的保存点恢复。

class GameState {
  private level: number = 1;
  private score: number = 0;

  setLevel(level: number): void {
    this.level = level;
  }

  setScore(score: number): void {
    this.score = score;
  }

  getLevel(): number {
    return this.level;
  }

  getScore(): number {
    return this.score;
  }

  createMemento(): GameMemento {
    return new GameMemento(this.level, this.score);
  }

  restoreMemento(memento: GameMemento): void {
    this.level = memento.getLevel();
    this.score = memento.getScore();
  }
}

// 备忘录
class GameMemento {
  private readonly level: number;
  private readonly score: number;

  constructor(level: number, score: number) {
    this.level = level;
    this.score = score;
  }

  getLevel(): number {
    return this.level;
  }

  getScore(): number {
    return this.score;
  }
}

// 备忘录管理者
class GameHistory {
  private mementos: GameMemento[] = [];

  saveMemento(memento: GameMemento): void {
    this.mementos.push(memento);
  }

  getMemento(index: number): GameMemento | undefined {
    return this.mementos[index];
  }
}

// 使用示例
const game = new GameState();
const gameHistory = new GameHistory();

game.setLevel(2);
game.setScore(500);
gameHistory.saveMemento(game.createMemento());

game.setLevel(3);
game.setScore(1500);
gameHistory.saveMemento(game.createMemento());

game.setLevel(4);
game.setScore(2000);

console.log("Current Level:", game.getLevel()); // 输出: Current Level: 4
console.log("Current Score:", game.getScore()); // 输出: Current Score: 2000

// 加载存档
const gameMemento1 = gameHistory.getMemento(1);
if (gameMemento1) {
  game.restoreMemento(gameMemento1);
}
console.log("After loading save 1 - Level:", game.getLevel()); // 输出: After loading save 1 - Level: 3
console.log("After loading save 1 - Score:", game.getScore()); // 输出: After loading save 1 - Score: 1500

// 再次加载存档
const gameMemento0 = gameHistory.getMemento(0);
if (gameMemento0) {
  game.restoreMemento(gameMemento0);
}
console.log("After loading save 0 - Level:", game.getLevel()); // 输出: After loading save 0 - Level: 2
console.log("After loading save 0 - Score:", game.getScore()); // 输出: After loading save 0 - Score: 500

SPA(单页应用)的状态管理

在复杂的 SPA 中,为了提升用户体验,可能需要保存某些视图/组件的状态,以便用户在返回时能够看到之前的状态。例如,用户在一个无限滚动列表中滚动了很长时间,然后跳到另一个页面,再返回时仍能保持之前的滚动位置和已加载的数据。

class ViewState {
  private scrollPosition: number = 0;

  setScrollPosition(position: number): void {
    this.scrollPosition = position;
  }

  getScrollPosition(): number {
    return this.scrollPosition;
  }

  createMemento(): ViewMemento {
    return new ViewMemento(this.scrollPosition);
  }

  restoreMemento(memento: ViewMemento): void {
    this.scrollPosition = memento.getScrollPosition();
  }
}

// 备忘录
class ViewMemento {
  private readonly scrollPosition: number;

  constructor(scrollPosition: number) {
    this.scrollPosition = scrollPosition;
  }

  getScrollPosition(): number {
    return this.scrollPosition;
  }
}

// 备忘录管理者
class ViewStateManager {
  private mementos: ViewMemento[] = [];

  saveMemento(memento: ViewMemento): void {
    this.mementos.push(memento);
  }

  getMemento(index: number): ViewMemento | undefined {
    return this.mementos[index];
  }
}

// 使用示例
const viewState = new ViewState();
const viewStateManager = new ViewStateManager();

// 用户滚动页面
window.addEventListener('scroll', () => {
  viewState.setScrollPosition(window.scrollY);
});

// 用户离开页面时保存滚动位置
window.addEventListener('beforeunload', () => {
  viewStateManager.saveMemento(viewState.createMemento());
});

// 用户返回页面时恢复滚动位置
window.addEventListener('load', () => {
  const latestMemento = viewStateManager.getMemento(viewStateManager.mementos.length - 1);
  if (latestMemento) {
    viewState.restoreMemento(latestMemento);
    window.scrollTo(0, viewState.getScrollPosition());
  }
});

组件/页面之间的状态迁移

在多步骤表单或向导样式的 UI 中,用户在每一步中输入的数据可以被视为一种状态,通过备忘录模式可以在用户操作过程中轻松前进或后退到任意步骤。

class FormStepState {
  private data: { [key: string]: any } = {};

  setData(key: string, value: any): void {
    this.data[key] = value;
  }

  getData(key: string): any {
    return this.data[key];
  }

  createMemento(): FormStepMemento {
    return new FormStepMemento({ ...this.data });
  }

  restoreMemento(memento: FormStepMemento): void {
    this.data = { ...memento.getData() };
  }
}

// 备忘录
class FormStepMemento {
  private readonly data: { [key: string]: any };

  constructor(data: { [key: string]: any }) {
    this.data = data;
  }

  getData(): { [key: string]: any } {
    return { ...this.data };
  }
}

// 备忘录管理者
class FormStepHistory {
  private mementos: FormStepMemento[] = [];

  saveMemento(memento: FormStepMemento): void {
    this.mementos.push(memento);
  }

  getMemento(index: number): FormStepMemento | undefined {
    return this.mementos[index];
  }
}

// 使用示例
const formStepState = new FormStepState();
const formStepHistory = new FormStepHistory();

// 用户填写第一步表单
formStepState.setData('step1', { name: 'John', email: '[email protected]' });
formStepHistory.saveMemento(formStepState.createMemento());

// 用户填写第二步表单
formStepState.setData('step2', { address: '123 Main St', city: 'Hometown' });
formStepHistory.saveMemento(formStepState.createMemento());

console.log('Current State:', formStepState.createMemento().getData()); // 输出: Current State: { step1: { name: 'John', email: '[email protected]' }, step2: { address: '123 Main St', city: 'Hometown' } }

// 用户返回第一步
const firstStepMemento = formStepHistory.getMemento(0);
if (firstStepMemento) {
  formStepState.restoreMemento(firstStepMemento);
}
console.log('After moving back to step 1:', formStepState.createMemento().getData()); // 输出: After moving back to step 1: { step1: { name: 'John', email: '[email protected]' } }

// 用户再次前进到第二步
const secondStepMemento = formStepHistory.getMemento(1);
if (secondStepMemento) {
  formStepState.restoreMemento(secondStepMemento);
}
console.log('After moving back to step 2:', formStepState.createMemento().getData()); // 输出: After moving back to step 2: { step1: { name: 'John', email: '[email protected]' }, step2: { address: '123 Main St', city: 'Hometown' } }

优缺点

优点

  1. 维护封装性:通过备忘录存储和恢复对象的状态,不需要暴露对象的内部结构和数据,维护了对象的封装性。
  2. 支持撤销和恢复:可以轻松实现对象状态的撤销和恢复功能,提高系统的用户体验。
  3. 易于扩展:可以轻松添加新的状态存储和恢复功能,扩展性强。

缺点

  1. 内存占用较大:如果状态信息较多或者频繁保存对象状态,可能会占用较多内存。
  2. 实现复杂性:需要额外的类来管理状态存储和恢复,可能增加代码复杂性。
  3. 性能问题:频繁创建和恢复备忘录对象可能带来性能问题,特别是在大规模应用中。

总结

备忘录模式通过在不破坏封装性的前提下,捕获和恢复对象的内部状态,为需要支持撤销和恢复操作的应用程序提供了一个灵活和易用的解决方案。它非常适合于实现撤销、恢复、事务管理、游戏存档等功能。