您现在的位置是:网站首页 > 命令模式(Command)的封装与撤销操作文章详情

命令模式(Command)的封装与撤销操作

命令模式的基本概念

命令模式是一种行为型设计模式,它将请求封装成对象,从而允许用户使用不同的请求、队列或日志请求来参数化其他对象。这种模式的核心思想是将"做什么"和"谁来做"解耦,使得请求的发送者和接收者之间不直接交互。

在JavaScript中,命令模式通常由一个命令对象实现,该对象包含一个执行方法(execute)和一个撤销方法(undo)。命令对象知道接收者(执行实际操作的对象)和调用接收者的方法。

class Command {
  constructor(receiver) {
    this.receiver = receiver;
  }
  
  execute() {
    throw new Error('必须实现execute方法');
  }
  
  undo() {
    throw new Error('必须实现undo方法');
  }
}

class LightOnCommand extends Command {
  constructor(light) {
    super(light);
    this.light = light;
  }
  
  execute() {
    this.light.on();
  }
  
  undo() {
    this.light.off();
  }
}

class Light {
  on() {
    console.log('灯亮了');
  }
  
  off() {
    console.log('灯灭了');
  }
}

// 使用示例
const light = new Light();
const lightOnCommand = new LightOnCommand(light);
lightOnCommand.execute(); // 输出: 灯亮了
lightOnCommand.undo();    // 输出: 灯灭了

命令模式的封装机制

命令模式通过将操作封装为对象,实现了操作的参数化和延迟执行。这种封装带来了几个显著优势:

  1. 解耦调用者和接收者:调用者不需要知道接收者的具体实现细节,只需要知道如何发送命令。
  2. 支持命令队列:可以很容易地将命令放入队列中,实现批处理或延迟执行。
  3. 支持日志和事务:可以记录命令的执行历史,便于实现撤销/重做功能。

一个更复杂的例子展示了如何封装多个操作:

class MacroCommand {
  constructor() {
    this.commands = [];
  }
  
  add(command) {
    this.commands.push(command);
  }
  
  execute() {
    this.commands.forEach(command => command.execute());
  }
  
  undo() {
    // 反向执行撤销操作
    [...this.commands].reverse().forEach(command => command.undo());
  }
}

// 使用示例
const light = new Light();
const tv = { 
  on() { console.log('电视打开了'); },
  off() { console.log('电视关闭了'); }
};

const lightOn = new LightOnCommand(light);
const tvOn = {
  execute() { tv.on(); },
  undo() { tv.off(); }
};

const macro = new MacroCommand();
macro.add(lightOn);
macro.add(tvOn);

macro.execute(); // 灯亮了 电视打开了
macro.undo();   // 电视关闭了 灯灭了

撤销操作的实现原理

命令模式实现撤销操作的核心在于:每个命令对象不仅知道如何执行操作,还知道如何撤销这个操作。通常有两种实现方式:

  1. 反向操作法:执行与原始操作相反的操作。例如,如果原始操作是"增加",则撤销操作就是"减少"。
  2. 状态恢复法:在执行操作前保存对象状态,撤销时恢复这个状态。

下面是一个使用状态恢复法实现撤销的编辑器示例:

class Editor {
  constructor() {
    this.content = '';
    this.clipboard = '';
  }
  
  write(text) {
    this.content += text;
  }
  
  copy() {
    this.clipboard = this.content;
  }
  
  paste() {
    this.content += this.clipboard;
  }
  
  getContent() {
    return this.content;
  }
}

class CopyCommand {
  constructor(editor) {
    this.editor = editor;
    this.previousClipboard = '';
  }
  
  execute() {
    this.previousClipboard = this.editor.clipboard;
    this.editor.copy();
  }
  
  undo() {
    this.editor.clipboard = this.previousClipboard;
  }
}

class PasteCommand {
  constructor(editor) {
    this.editor = editor;
    this.previousContent = '';
  }
  
  execute() {
    this.previousContent = this.editor.getContent();
    this.editor.paste();
  }
  
  undo() {
    this.editor.content = this.previousContent;
  }
}

// 使用示例
const editor = new Editor();
editor.write('Hello ');

const copyCmd = new CopyCommand(editor);
copyCmd.execute();

const pasteCmd = new PasteCommand(editor);
pasteCmd.execute(); // 内容变为 "Hello Hello "
pasteCmd.undo();   // 恢复为 "Hello "

复杂撤销栈的实现

在实际应用中,通常需要维护一个撤销栈来支持多级撤销。下面是一个完整的撤销/重做管理器的实现:

class CommandManager {
  constructor() {
    this.undoStack = [];
    this.redoStack = [];
    this.maxStackSize = 100; // 防止内存泄漏
  }
  
  execute(command) {
    command.execute();
    this.undoStack.push(command);
    this.redoStack = []; // 执行新命令时清空重做栈
    
    // 限制栈大小
    if (this.undoStack.length > this.maxStackSize) {
      this.undoStack.shift();
    }
  }
  
  undo() {
    if (this.undoStack.length === 0) return;
    
    const command = this.undoStack.pop();
    command.undo();
    this.redoStack.push(command);
  }
  
  redo() {
    if (this.redoStack.length === 0) return;
    
    const command = this.redoStack.pop();
    command.execute();
    this.undoStack.push(command);
  }
  
  clear() {
    this.undoStack = [];
    this.redoStack = [];
  }
}

// 使用示例
const editor = new Editor();
const cmdManager = new CommandManager();

editor.write('初始内容');
console.log(editor.getContent()); // "初始内容"

const writeCmd = {
  execute: () => editor.write('添加的文字'),
  undo: () => { editor.content = editor.content.slice(0, -'添加的文字'.length); }
};

cmdManager.execute(writeCmd);
console.log(editor.getContent()); // "初始内容添加的文字"

cmdManager.undo();
console.log(editor.getContent()); // "初始内容"

cmdManager.redo();
console.log(editor.getContent()); // "初始内容添加的文字"

命令模式在前端框架中的应用

命令模式在前端框架中有广泛应用,特别是在状态管理和用户交互处理方面。以下是几个典型应用场景:

  1. Redux中的action:Redux的action本质上是命令模式的实现,每个action描述了一个状态变更请求。
  2. Vuex的mutation:Vuex中的mutation也是命令模式的应用,它封装了状态变更操作。
  3. UI组件操作:按钮点击、菜单选择等用户交互通常可以抽象为命令。

下面是一个模拟Redux中使用命令模式的例子:

// 模拟Redux store
class Store {
  constructor(reducer, initialState) {
    this.state = initialState;
    this.reducer = reducer;
    this.history = [];
    this.historyIndex = -1;
  }
  
  dispatch(action) {
    // 保存当前状态用于撤销
    if (this.historyIndex < this.history.length - 1) {
      this.history = this.history.slice(0, this.historyIndex + 1);
    }
    this.history.push(this.state);
    this.historyIndex++;
    
    this.state = this.reducer(this.state, action);
  }
  
  undo() {
    if (this.historyIndex <= 0) return;
    
    this.historyIndex--;
    this.state = this.history[this.historyIndex];
  }
  
  redo() {
    if (this.historyIndex >= this.history.length - 1) return;
    
    this.historyIndex++;
    this.state = this.history[this.historyIndex];
  }
  
  getState() {
    return this.state;
  }
}

// reducer函数
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

// 使用示例
const store = new Store(counterReducer, { count: 0 });

store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // { count: 2 }

store.undo();
console.log(store.getState()); // { count: 1 }

store.redo();
console.log(store.getState()); // { count: 2 }

命令模式与异步操作

命令模式也可以很好地处理异步操作,只需在命令对象中实现异步的execute和undo方法。下面是一个处理AJAX请求的命令示例:

class ApiCommand {
  constructor(apiClient, endpoint, data) {
    this.apiClient = apiClient;
    this.endpoint = endpoint;
    this.data = data;
    this.previousData = null;
  }
  
  async execute() {
    try {
      // 保存当前状态以便撤销
      const response = await this.apiClient.get(this.endpoint);
      this.previousData = response.data;
      
      // 执行更新操作
      await this.apiClient.post(this.endpoint, this.data);
      return true;
    } catch (error) {
      console.error('执行失败:', error);
      return false;
    }
  }
  
  async undo() {
    if (!this.previousData) {
      console.error('无法撤销: 没有之前的记录');
      return false;
    }
    
    try {
      await this.apiClient.post(this.endpoint, this.previousData);
      return true;
    } catch (error) {
      console.error('撤销失败:', error);
      return false;
    }
  }
}

// 模拟API客户端
const mockApiClient = {
  dataStore: {
    '/user': { name: 'John', age: 30 }
  },
  
  get(endpoint) {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve({ data: { ...this.dataStore[endpoint] } });
      }, 100);
    });
  },
  
  post(endpoint, data) {
    return new Promise(resolve => {
      setTimeout(() => {
        this.dataStore[endpoint] = data;
        resolve();
      }, 100);
    });
  }
};

// 使用示例
(async () => {
  const updateUserCmd = new ApiCommand(
    mockApiClient,
    '/user',
    { name: 'Mike', age: 25 }
  );
  
  await updateUserCmd.execute();
  console.log(mockApiClient.dataStore['/user']); // { name: 'Mike', age: 25 }
  
  await updateUserCmd.undo();
  console.log(mockApiClient.dataStore['/user']); // { name: 'John', age: 30 }
})();

命令模式的性能考量

虽然命令模式提供了很大的灵活性,但在实现时需要考虑一些性能问题:

  1. 内存使用:每个命令对象都会占用内存,特别是在需要支持多级撤销时,保存的状态可能会占用大量内存。
  2. 执行效率:某些命令可能需要复杂的撤销逻辑,这会影响性能。
  3. 序列化开销:如果需要将命令序列化(例如用于持久化或网络传输),可能会引入额外的开销。

下面是一个考虑性能优化的命令实现示例,使用差异记录而不是完整状态保存:

class EfficientTextEditor {
  constructor() {
    this.content = '';
    this.version = 0;
    this.changeLog = [];
  }
  
  insert(text, position) {
    // 记录变更差异而不是完整内容
    this.changeLog.push({
      type: 'insert',
      text,
      position,
      version: ++this.version
    });
    
    this.content = this.content.slice(0, position) + text + this.content.slice(position);
  }
  
  delete(position, length) {
    const deletedText = this.content.slice(position, position + length);
    this.changeLog.push({
      type: 'delete',
      text: deletedText,
      position,
      version: ++this.version
    });
    
    this.content = this.content.slice(0, position) + this.content.slice(position + length);
  }
  
  undo() {
    if (this.changeLog.length === 0) return;
    
    const lastChange = this.changeLog.pop();
    this.version--;
    
    if (lastChange.type === 'insert') {
      // 撤销插入操作就是删除插入的文本
      this.content = this.content.slice(0, lastChange.position) + 
                    this.content.slice(lastChange.position + lastChange.text.length);
    } else if (lastChange.type === 'delete') {
      // 撤销删除操作就是重新插入被删除的文本
      this.content = this.content.slice(0, lastChange.position) + 
                    lastChange.text + 
                    this.content.slice(lastChange.position);
    }
  }
  
  getContent() {
    return this.content;
  }
}

// 使用示例
const editor = new EfficientTextEditor();
editor.insert('Hello', 0);
editor.insert(' World', 5);
console.log(editor.getContent()); // "Hello World"

editor.delete(5, 6);
console.log(editor.getContent()); // "Hello"

editor.undo();
console.log(editor.getContent()); // "Hello World"

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步