Skip to content

备忘录模式(Memento Pattern)

概念

备忘录模式是一种行为设计模式,它允许在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便日后可以将该对象恢复到原先保存的状态。

备忘录模式也被称为快照模式(Snapshot Pattern)或令牌模式(Token Pattern)。它主要用于实现撤销操作、恢复功能或者状态保存功能,常见于文本编辑器、游戏存档、数据库事务等场景。

基本实现

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

  1. Originator(发起人):负责创建一个备忘录来记录当前时刻自身的内部状态,并可使用备忘录恢复内部状态。
  2. Memento(备忘录):负责存储Originator对象的内部状态,防止Originator以外的其他对象访问备忘录。
  3. Caretaker(管理者):负责存储备忘录,但不能对备忘录的内容进行访问或者操作。

下面是备忘录模式的基本实现:

javascript
// 备忘录类
class Memento {
  constructor(state) {
    // 使用私有字段存储状态,防止外部访问
    this._state = state;
  }

  // 只有Originator可以访问状态
  getState() {
    return this._state;
  }
}

// 发起人类
class Originator {
  constructor() {
    this.state = '';
  }

  setState(state) {
    console.log(`设置状态: ${state}`);
    this.state = state;
  }

  getState() {
    return this.state;
  }

  // 创建备忘录
  createMemento() {
    console.log(`创建备忘录,保存状态: ${this.state}`);
    return new Memento(this.state);
  }

  // 从备忘录恢复状态
  restoreMemento(memento) {
    this.state = memento.getState();
    console.log(`从备忘录恢复状态: ${this.state}`);
  }
}

// 管理者类
class Caretaker {
  constructor() {
    this.mementos = [];
  }

  addMemento(memento) {
    this.mementos.push(memento);
  }

  getMemento(index) {
    return this.mementos[index];
  }

  getSize() {
    return this.mementos.length;
  }
}

// 使用示例
const originator = new Originator();
const caretaker = new Caretaker();

// 设置状态并保存备忘录
originator.setState('State 1');
caretaker.addMemento(originator.createMemento());

originator.setState('State 2');
caretaker.addMemento(originator.createMemento());

originator.setState('State 3');
console.log(`当前状态: ${originator.getState()}`);

// 恢复到之前的状态
console.log('\n恢复到第一个状态:');
originator.restoreMemento(caretaker.getMemento(0));
console.log(`当前状态: ${originator.getState()}`);

实际应用场景

场景1:文本编辑器的撤销功能

文本编辑器是最典型的备忘录模式应用场景,可以保存编辑历史并支持撤销操作。

javascript
// 文本编辑器状态备忘录
class TextEditorMemento {
  constructor(content, cursorPosition, fontSize) {
    this._content = content;
    this._cursorPosition = cursorPosition;
    this._fontSize = fontSize;
    this._timestamp = new Date();
  }

  getContent() {
    return this._content;
  }

  getCursorPosition() {
    return this._cursorPosition;
  }

  getFontSize() {
    return this._fontSize;
  }

  getTimestamp() {
    return this._timestamp;
  }
}

// 文本编辑器
class TextEditor {
  constructor() {
    this.content = '';
    this.cursorPosition = 0;
    this.fontSize = 14;
  }

  setContent(content) {
    this.content = content;
  }

  setCursorPosition(position) {
    this.cursorPosition = position;
  }

  setFontSize(size) {
    this.fontSize = size;
  }

  getContent() {
    return this.content;
  }

  getCursorPosition() {
    return this.cursorPosition;
  }

  getFontSize() {
    return this.fontSize;
  }

  // 创建备忘录
  createMemento() {
    return new TextEditorMemento(
      this.content,
      this.cursorPosition,
      this.fontSize
    );
  }

  // 从备忘录恢复
  restoreMemento(memento) {
    this.content = memento.getContent();
    this.cursorPosition = memento.getCursorPosition();
    this.fontSize = memento.getFontSize();
  }

  // 显示当前状态
  display() {
    console.log(`内容: "${this.content}"`);
    console.log(`光标位置: ${this.cursorPosition}`);
    console.log(`字体大小: ${this.fontSize}`);
  }
}

// 历史管理器
class HistoryManager {
  constructor(maxSnapshots = 10) {
    this.snapshots = [];
    this.currentIndex = -1;
    this.maxSnapshots = maxSnapshots;
  }

  // 保存快照
  saveSnapshot(memento) {
    // 如果当前不在最新状态,删除之后的历史
    if (this.currentIndex < this.snapshots.length - 1) {
      this.snapshots = this.snapshots.slice(0, this.currentIndex + 1);
    }

    // 添加新快照
    this.snapshots.push(memento);
    
    // 控制历史记录数量
    if (this.snapshots.length > this.maxSnapshots) {
      this.snapshots.shift();
    } else {
      this.currentIndex++;
    }
    
    console.log(`保存快照 (${this.snapshots.length}/${this.maxSnapshots})`);
  }

  // 撤销操作
  undo() {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      return this.snapshots[this.currentIndex];
    }
    console.log('已经到达最早的历史记录');
    return null;
  }

  // 重做操作
  redo() {
    if (this.currentIndex < this.snapshots.length - 1) {
      this.currentIndex++;
      return this.snapshots[this.currentIndex];
    }
    console.log('已经到达最新的历史记录');
    return null;
  }

  // 获取当前快照
  getCurrentSnapshot() {
    if (this.currentIndex >= 0 && this.currentIndex < this.snapshots.length) {
      return this.snapshots[this.currentIndex];
    }
    return null;
  }
}

// 使用示例
const editor = new TextEditor();
const history = new HistoryManager(5);

console.log('=== 编辑操作 ===');
// 第一次编辑
editor.setContent('Hello');
editor.setCursorPosition(5);
editor.setFontSize(16);
history.saveSnapshot(editor.createMemento());
editor.display();

console.log('\n---');

// 第二次编辑
editor.setContent('Hello World');
editor.setCursorPosition(11);
editor.setFontSize(18);
history.saveSnapshot(editor.createMemento());
editor.display();

console.log('\n---');

// 第三次编辑
editor.setContent('Hello World!');
editor.setCursorPosition(12);
editor.setFontSize(20);
history.saveSnapshot(editor.createMemento());
editor.display();

console.log('\n=== 撤销操作 ===');
// 撤销一次
let memento = history.undo();
if (memento) {
  editor.restoreMemento(memento);
  console.log('撤销后:');
  editor.display();
}

console.log('\n---');

// 再次撤销
memento = history.undo();
if (memento) {
  editor.restoreMemento(memento);
  console.log('再次撤销后:');
  editor.display();
}

console.log('\n=== 重做操作 ===');
// 重做一次
memento = history.redo();
if (memento) {
  editor.restoreMemento(memento);
  console.log('重做后:');
  editor.display();
}

场景2:游戏存档系统

在电子游戏中,玩家的游戏进度需要定期保存,以便在需要时可以恢复到之前的某个状态。

javascript
// 游戏状态备忘录
class GameStateMemento {
  constructor(level, score, health, inventory, position) {
    this._level = level;
    this._score = score;
    this._health = health;
    this._inventory = [...inventory]; // 复制数组
    this._position = { ...position }; // 复制对象
    this._timestamp = new Date();
  }

  getLevel() {
    return this._level;
  }

  getScore() {
    return this._score;
  }

  getHealth() {
    return this._health;
  }

  getInventory() {
    return [...this._inventory];
  }

  getPosition() {
    return { ...this._position };
  }

  getTimestamp() {
    return this._timestamp;
  }

  getSummary() {
    return `关卡: ${this._level}, 分数: ${this._score}, 生命值: ${this._health}`;
  }
}

// 游戏角色类
class GameCharacter {
  constructor(name) {
    this.name = name;
    this.level = 1;
    this.score = 0;
    this.health = 100;
    this.inventory = [];
    this.position = { x: 0, y: 0 };
  }

  // 升级
  levelUp() {
    this.level++;
    this.score += 100;
    console.log(`${this.name} 升到了 ${this.level} 级!`);
  }

  // 获得分数
  addScore(points) {
    this.score += points;
    console.log(`${this.name} 获得了 ${points} 分,总分: ${this.score}`);
  }

  // 受伤
  takeDamage(damage) {
    this.health -= damage;
    if (this.health < 0) this.health = 0;
    console.log(`${this.name} 受到 ${damage} 点伤害,剩余生命值: ${this.health}`);
  }

  // 治疗
  heal(amount) {
    this.health += amount;
    if (this.health > 100) this.health = 100;
    console.log(`${this.name} 恢复了 ${amount} 点生命值,当前生命值: ${this.health}`);
  }

  // 添加物品到背包
  addItem(item) {
    this.inventory.push(item);
    console.log(`${this.name} 获得了 ${item}`);
  }

  // 移动
  moveTo(x, y) {
    this.position.x = x;
    this.position.y = y;
    console.log(`${this.name} 移动到位置 (${x}, ${y})`);
  }

  // 创建游戏状态备忘录
  createMemento() {
    console.log(`${this.name} 创建游戏存档`);
    return new GameStateMemento(
      this.level,
      this.score,
      this.health,
      this.inventory,
      this.position
    );
  }

  // 从备忘录恢复游戏状态
  restoreMemento(memento) {
    this.level = memento.getLevel();
    this.score = memento.getScore();
    this.health = memento.getHealth();
    this.inventory = memento.getInventory();
    this.position = memento.getPosition();
    console.log(`${this.name} 从存档恢复游戏状态`);
  }

  // 显示当前状态
  displayStatus() {
    console.log(`\n=== ${this.name} 当前状态 ===`);
    console.log(`关卡: ${this.level}`);
    console.log(`分数: ${this.score}`);
    console.log(`生命值: ${this.health}`);
    console.log(`位置: (${this.position.x}, ${this.position.y})`);
    console.log(`背包: [${this.inventory.join(', ')}]`);
  }
}

// 存档管理系统
class SaveManager {
  constructor() {
    this.saves = new Map(); // 使用Map存储带名称的存档
  }

  // 保存游戏进度
  saveGame(name, memento) {
    this.saves.set(name, {
      memento: memento,
      slot: name
    });
    console.log(`游戏进度已保存到存档 "${name}"`);
  }

  // 加载游戏进度
  loadGame(name) {
    const save = this.saves.get(name);
    if (save) {
      console.log(`从存档 "${name}" 加载游戏进度`);
      return save.memento;
    }
    console.log(`存档 "${name}" 不存在`);
    return null;
  }

  // 删除存档
  deleteSave(name) {
    if (this.saves.delete(name)) {
      console.log(`存档 "${name}" 已删除`);
    } else {
      console.log(`存档 "${name}" 不存在`);
    }
  }

  // 列出所有存档
  listSaves() {
    console.log('\n=== 存档列表 ===');
    if (this.saves.size === 0) {
      console.log('暂无存档');
      return;
    }

    for (const [name, save] of this.saves) {
      console.log(`存档名: ${name}`);
      console.log(`  ${save.memento.getSummary()}`);
      console.log(`  时间: ${save.memento.getTimestamp().toLocaleString()}`);
    }
  }
}

// 使用示例
const player = new GameCharacter('Hero');
const saveManager = new SaveManager();

console.log('=== 游戏开始 ===');
player.displayStatus();

console.log('\n--- 进行游戏 ---');
player.moveTo(10, 15);
player.addItem('剑');
player.addScore(50);
player.levelUp();

player.displayStatus();
saveManager.saveGame('autosave', player.createMemento());

console.log('\n--- 继续冒险 ---');
player.moveTo(25, 30);
player.addItem('药水');
player.addScore(75);
player.takeDamage(30);
player.heal(10);

player.displayStatus();
saveManager.saveGame('checkpoint1', player.createMemento());

console.log('\n--- 遭遇强敌 ---');
player.takeDamage(80);
player.addItem('宝箱钥匙');
player.addScore(200);

player.displayStatus();
saveManager.saveGame('before_boss', player.createMemento());

console.log('\n--- 不幸阵亡 ---');
player.takeDamage(50); // 生命值降为0
player.displayStatus();

console.log('\n=== 恢复游戏 ===');
saveManager.listSaves();

// 从检查点恢复
const checkpointMemento = saveManager.loadGame('checkpoint1');
if (checkpointMemento) {
  player.restoreMemento(checkpointMemento);
  player.displayStatus();
}

场景3:表单数据自动保存

在网页表单中,可以使用备忘录模式定期保存用户输入的数据,防止意外刷新导致数据丢失。

javascript
// 表单数据备忘录
class FormMemento {
  constructor(formData, timestamp = new Date()) {
    this._formData = { ...formData };
    this._timestamp = timestamp;
  }

  getFormData() {
    return { ...this._formData };
  }

  getTimestamp() {
    return this._timestamp;
  }

  getTimeAgo() {
    const now = new Date();
    const diff = now - this._timestamp;
    const minutes = Math.floor(diff / 60000);
    const hours = Math.floor(diff / 3600000);
    
    if (hours > 0) {
      return `${hours}小时前`;
    }
    if (minutes > 0) {
      return `${minutes}分钟前`;
    }
    return '刚刚';
  }
}

// 表单类
class Form {
  constructor(formId) {
    this.formId = formId;
    this.fields = {};
  }

  // 设置字段值
  setField(name, value) {
    this.fields[name] = value;
  }

  // 获取字段值
  getField(name) {
    return this.fields[name];
  }

  // 获取所有字段
  getAllFields() {
    return { ...this.fields };
  }

  // 从对象设置多个字段
  setFields(fields) {
    Object.assign(this.fields, fields);
  }

  // 创建备忘录
  createMemento() {
    return new FormMemento(this.fields);
  }

  // 从备忘录恢复
  restoreMemento(memento) {
    this.fields = memento.getFormData();
  }

  // 显示表单数据
  display() {
    console.log(`\n=== 表单 ${this.formId} 数据 ===`);
    for (const [name, value] of Object.entries(this.fields)) {
      console.log(`${name}: ${value}`);
    }
  }
}

// 表单自动保存管理器
class FormAutoSaveManager {
  constructor(form, interval = 30000) { // 默认30秒自动保存
    this.form = form;
    this.history = [];
    this.maxHistory = 10; // 最大历史记录数
    this.interval = interval;
    this.timer = null;
    this.isAutoSaving = false;
  }

  // 开始自动保存
  startAutoSave() {
    if (this.timer) {
      clearInterval(this.timer);
    }
    
    this.isAutoSaving = true;
    this.timer = setInterval(() => {
      this.saveSnapshot();
    }, this.interval);
    
    console.log(`开始自动保存,间隔: ${this.interval / 1000}秒`);
  }

  // 停止自动保存
  stopAutoSave() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
      this.isAutoSaving = false;
      console.log('停止自动保存');
    }
  }

  // 手动保存快照
  saveSnapshot() {
    const memento = this.form.createMemento();
    this.history.push(memento);
    
    // 限制历史记录数量
    if (this.history.length > this.maxHistory) {
      this.history.shift();
    }
    
    console.log(`表单数据已保存 (${this.history.length}/${this.maxHistory}) ${memento.getTimeAgo()}`);
    return memento;
  }

  // 获取历史记录
  getHistory() {
    return [...this.history];
  }

  // 恢复到指定历史记录
  restoreFromHistory(index) {
    if (index >= 0 && index < this.history.length) {
      const memento = this.history[index];
      this.form.restoreMemento(memento);
      console.log(`已恢复到 ${memento.getTimeAgo()} 的数据`);
      return true;
    }
    console.log('无效的历史记录索引');
    return false;
  }

  // 恢复到最近的历史记录
  restoreToLatest() {
    if (this.history.length > 0) {
      const latest = this.history[this.history.length - 1];
      this.form.restoreMemento(latest);
      console.log(`已恢复到最近保存的数据 (${latest.getTimeAgo()})`);
      return true;
    }
    console.log('没有可用的历史记录');
    return false;
  }

  // 显示保存历史
  showHistory() {
    console.log('\n=== 表单保存历史 ===');
    if (this.history.length === 0) {
      console.log('暂无保存记录');
      return;
    }

    this.history.forEach((memento, index) => {
      console.log(`${index + 1}. ${memento.getTimeAgo()}`);
    });
  }
}

// 使用示例
const form = new Form('registration');
const autoSaveManager = new FormAutoSaveManager(form, 10000); // 10秒间隔

console.log('=== 填写表单 ===');
form.setField('name', '张三');
form.setField('email', 'zhangsan@example.com');
form.setField('phone', '13800138000');
form.display();

// 手动保存
autoSaveManager.saveSnapshot();

console.log('\n--- 继续填写 ---');
form.setField('address', '北京市朝阳区');
form.setField('company', '某某科技有限公司');
form.display();

// 手动保存
autoSaveManager.saveSnapshot();

console.log('\n--- 修改信息 ---');
form.setField('email', 'zhangsan@newdomain.com');
form.setField('position', '前端工程师');
form.display();

// 手动保存
autoSaveManager.saveSnapshot();

console.log('\n=== 查看保存历史 ===');
autoSaveManager.showHistory();

console.log('\n=== 恢复到早期版本 ===');
// 恢复到第一个保存点
autoSaveManager.restoreFromHistory(0);
form.display();

console.log('\n--- 恢复到最近版本 ---');
autoSaveManager.restoreToLatest();
form.display();

// 开始自动保存
autoSaveManager.startAutoSave();

// 模拟继续填写表单
setTimeout(() => {
  form.setField('experience', '5年');
  form.setField('skills', 'JavaScript, React, Vue');
  form.display();
}, 15000);

// 25秒后停止自动保存
setTimeout(() => {
  autoSaveManager.stopAutoSave();
  console.log('\n=== 最终保存历史 ===');
  autoSaveManager.showHistory();
}, 25000);

关键要点

  1. 保护性拷贝:备忘录应该对其他对象隐藏其内部状态,只能由原发器访问。

  2. 状态管理:需要考虑备忘录的存储和管理策略,如存储容量限制、过期清理等。

  3. 性能考量:频繁创建备忘录可能会消耗大量内存,需要权衡保存频率和资源消耗。

  4. 深拷贝vs浅拷贝:根据具体需求决定使用深拷贝还是浅拷贝来保存状态。

与其他模式的关系

  • 与命令模式结合:命令模式的撤销操作常常借助备忘录模式来保存和恢复状态。
  • 与原型模式结合:备忘录可以使用原型模式来实现状态的复制。
  • 与单例模式结合:管理者可以使用单例模式确保全局只有一个管理实例。

优缺点

优点:

  1. 保护封装性:备忘录模式可以避免暴露对象的内部状态,保护对象的封装性。
  2. 简化原发器类:原发器不需要保存历史状态的逻辑,只需创建和使用备忘录。
  3. 支持撤销操作:可以方便地实现撤销功能,提升用户体验。

缺点:

  1. 消耗内存:如果需要保存大量的状态信息,会消耗较多的内存资源。
  2. 可能破坏封装性:在某些实现中,为了能让原发器访问状态,可能需要放宽备忘录的访问权限。
  3. 管理复杂性:需要合理管理备忘录的生命周期,避免内存泄漏。

备忘录模式在需要保存和恢复对象状态的场景中非常有用,特别是在实现撤销操作、历史记录管理和数据备份等方面。正确使用备忘录模式可以显著提升应用程序的用户体验和数据安全性。