阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 命令模式(Command)的封装与撤销操作

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

作者:陈川 阅读数:48612人阅读 分类: JavaScript

命令模式的基本概念

命令模式是一种行为型设计模式,它将请求封装成对象,从而允许用户使用不同的请求、队列或日志来参数化其他对象。这种模式的核心思想是将"做什么"(具体操作)与"谁来做"(调用者)解耦。在JavaScript中,命令模式通常表现为一个包含执行方法的对象。

class Command {
  execute() {
    throw new Error('必须实现execute方法');
  }
}

class ConcreteCommand extends Command {
  constructor(receiver) {
    super();
    this.receiver = receiver;
  }
  
  execute() {
    this.receiver.action();
  }
}

class Receiver {
  action() {
    console.log('执行具体操作');
  }
}

const receiver = new Receiver();
const command = new ConcreteCommand(receiver);
command.execute(); // 输出: 执行具体操作

命令模式的封装特性

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

  1. 请求的封装:将请求的细节隐藏在命令对象内部
  2. 调用者与接收者解耦:调用者不需要知道接收者的具体实现
  3. 支持复合命令:可以将多个命令组合成一个复合命令
// 复合命令示例
class MacroCommand {
  constructor() {
    this.commands = [];
  }
  
  add(command) {
    this.commands.push(command);
  }
  
  execute() {
    this.commands.forEach(command => command.execute());
  }
}

const macro = new MacroCommand();
macro.add(new ConcreteCommand(receiver));
macro.add(new ConcreteCommand(receiver));
macro.execute(); // 会执行两次receiver.action()

实现撤销操作

命令模式天然支持撤销(undo)操作,这是它的一个重要应用场景。要实现撤销功能,命令对象需要存储足够的状态信息以便回滚操作。

class UndoableCommand extends Command {
  constructor(receiver, value) {
    super();
    this.receiver = receiver;
    this.value = value;
    this.previousValue = null;
  }
  
  execute() {
    this.previousValue = this.receiver.value;
    this.receiver.value = this.value;
    console.log(`设置值为: ${this.value}`);
  }
  
  undo() {
    this.receiver.value = this.previousValue;
    console.log(`撤销到: ${this.previousValue}`);
  }
}

class ReceiverWithState {
  constructor() {
    this.value = 0;
  }
}

const receiverWithState = new ReceiverWithState();
const undoableCommand = new UndoableCommand(receiverWithState, 10);

undoableCommand.execute(); // 设置值为: 10
console.log(receiverWithState.value); // 10
undoableCommand.undo();    // 撤销到: 0
console.log(receiverWithState.value); // 0

更复杂的撤销栈实现

在实际应用中,我们通常需要维护一个完整的撤销栈,支持多步撤销和重做:

class CommandManager {
  constructor() {
    this.undoStack = [];
    this.redoStack = [];
  }
  
  execute(command) {
    command.execute();
    this.undoStack.push(command);
    this.redoStack = []; // 执行新命令时清空重做栈
  }
  
  undo() {
    if (this.undoStack.length > 0) {
      const command = this.undoStack.pop();
      command.undo();
      this.redoStack.push(command);
    }
  }
  
  redo() {
    if (this.redoStack.length > 0) {
      const command = this.redoStack.pop();
      command.execute();
      this.undoStack.push(command);
    }
  }
}

// 使用示例
const manager = new CommandManager();
const receiver = new ReceiverWithState();

manager.execute(new UndoableCommand(receiver, 10));
manager.execute(new UndoableCommand(receiver, 20));
console.log(receiver.value); // 20

manager.undo();
console.log(receiver.value); // 10

manager.undo();
console.log(receiver.value); // 0

manager.redo();
console.log(receiver.value); // 10

命令模式在UI交互中的应用

命令模式在前端开发中特别适合处理用户交互,比如按钮点击、菜单选择等操作。下面是一个实际的DOM操作示例:

// DOM操作命令
class DOMCommand {
  constructor(element, property, newValue) {
    this.element = element;
    this.property = property;
    this.newValue = newValue;
    this.oldValue = null;
  }
  
  execute() {
    this.oldValue = this.element.style[this.property];
    this.element.style[this.property] = this.newValue;
  }
  
  undo() {
    this.element.style[this.property] = this.oldValue;
  }
}

// 使用示例
const button = document.createElement('button');
button.textContent = '点击我';
document.body.appendChild(button);

const commandManager = new CommandManager();
const changeColorCmd = new DOMCommand(button, 'backgroundColor', 'red');
const changeTextCmd = new DOMCommand(button, 'color', 'white');

// 执行命令
commandManager.execute(changeColorCmd);
commandManager.execute(changeTextCmd);

// 添加撤销按钮
const undoBtn = document.createElement('button');
undoBtn.textContent = '撤销';
undoBtn.addEventListener('click', () => commandManager.undo());
document.body.appendChild(undoBtn);

// 添加重做按钮
const redoBtn = document.createElement('button');
redoBtn.textContent = '重做';
redoBtn.addEventListener('click', () => commandManager.redo());
document.body.appendChild(redoBtn);

命令队列与延迟执行

命令模式还支持将命令放入队列中,按需执行或延迟执行,这在动画、批量操作等场景中非常有用:

class CommandQueue {
  constructor() {
    this.queue = [];
    this.isExecuting = false;
  }
  
  add(command) {
    this.queue.push(command);
    if (!this.isExecuting) {
      this.executeNext();
    }
  }
  
  executeNext() {
    if (this.queue.length > 0) {
      this.isExecuting = true;
      const command = this.queue.shift();
      command.execute();
      
      // 模拟异步操作完成
      setTimeout(() => {
        this.isExecuting = false;
        this.executeNext();
      }, 1000);
    }
  }
}

// 使用示例
const queue = new CommandQueue();
const receiver = {
  action: (msg) => console.log(`执行: ${msg}`)
};

class LogCommand {
  constructor(receiver, message) {
    this.receiver = receiver;
    this.message = message;
  }
  
  execute() {
    this.receiver.action(this.message);
  }
}

queue.add(new LogCommand(receiver, '第一个命令'));
queue.add(new LogCommand(receiver, '第二个命令'));
queue.add(new LogCommand(receiver, '第三个命令'));

// 输出:
// 执行: 第一个命令
// (1秒后)执行: 第二个命令
// (再过1秒)执行: 第三个命令

命令模式的变体与应用场景

命令模式有多种变体,适用于不同场景:

  1. 简单命令:只包含执行方法的基本命令
  2. 可撤销命令:包含undo方法的命令
  3. 事务性命令:要么全部执行成功,要么全部回滚
  4. 宏命令:组合多个命令作为一个命令执行

典型应用场景包括:

  • GUI按钮和菜单项
  • 事务处理系统
  • 进度条操作
  • 多级撤销/重做功能
  • 日志记录系统
  • 任务调度系统
// 事务性命令示例
class Transaction {
  constructor() {
    this.commands = [];
    this.executed = false;
  }
  
  add(command) {
    if (this.executed) {
      throw new Error('事务已执行,不能添加新命令');
    }
    this.commands.push(command);
  }
  
  execute() {
    if (this.executed) return;
    
    try {
      this.commands.forEach(cmd => cmd.execute());
      this.executed = true;
    } catch (error) {
      // 执行失败则回滚已执行的命令
      for (let i = this.commands.length - 1; i >= 0; i--) {
        if (this.commands[i].undo) {
          this.commands[i].undo();
        }
      }
      throw error;
    }
  }
}

// 使用示例
const transaction = new Transaction();
transaction.add(new UndoableCommand(receiverWithState, 10));
transaction.add(new UndoableCommand(receiverWithState, 20));

try {
  transaction.execute();
  console.log(receiverWithState.value); // 20
} catch (error) {
  console.error('事务执行失败:', error);
}

命令模式与内存管理

在使用命令模式实现撤销/重做功能时,需要注意内存管理问题。长时间运行的应用程序可能会积累大量命令对象,导致内存占用过高。解决方案包括:

  1. 限制历史记录长度:只保留最近的N条命令
  2. 快照模式:定期保存完整状态而不是记录每个命令
  3. 命令压缩:将多个连续命令合并为一个
// 限制历史记录长度的命令管理器
class LimitedCommandManager extends CommandManager {
  constructor(limit = 50) {
    super();
    this.limit = limit;
  }
  
  execute(command) {
    super.execute(command);
    if (this.undoStack.length > this.limit) {
      this.undoStack.shift(); // 移除最旧的命令
    }
  }
}

// 快照命令示例
class SnapshotCommand {
  constructor(receiver) {
    this.receiver = receiver;
    this.snapshot = null;
    this.newState = null;
  }
  
  execute(newState) {
    this.snapshot = JSON.stringify(this.receiver.state);
    this.receiver.state = newState;
    this.newState = JSON.stringify(newState);
  }
  
  undo() {
    if (this.snapshot) {
      this.receiver.state = JSON.parse(this.snapshot);
    }
  }
  
  redo() {
    if (this.newState) {
      this.receiver.state = JSON.parse(this.newState);
    }
  }
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌