阿里云主机折上折
  • 微信号
Current Site:Index > Encapsulation and undo operations in the Command pattern

Encapsulation and undo operations in the Command pattern

Author:Chuan Chen 阅读数:48613人阅读 分类: JavaScript

Basic Concepts of the Command Pattern

The Command pattern is a behavioral design pattern that encapsulates requests as objects, thereby allowing users to parameterize other objects with different requests, queues, or logs. The core idea of this pattern is to decouple "what to do" (specific operations) from "who does it" (the invoker). In JavaScript, the Command pattern typically manifests as an object containing an execution method.

class Command {
  execute() {
    throw new Error('The execute method must be implemented');
  }
}

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

class Receiver {
  action() {
    console.log('Perform specific operation');
  }
}

const receiver = new Receiver();
const command = new ConcreteCommand(receiver);
command.execute(); // Output: Perform specific operation

Encapsulation Characteristics of the Command Pattern

The Command pattern encapsulates operations as objects, enabling parameterization and delayed execution of operations. This encapsulation offers several notable advantages:

  1. Encapsulation of Requests: Hides the details of requests within command objects.
  2. Decoupling Invoker and Receiver: The invoker does not need to know the specific implementation of the receiver.
  3. Support for Composite Commands: Multiple commands can be combined into a single composite command.
// Composite Command Example
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(); // Executes receiver.action() twice

Implementing Undo Operations

The Command pattern naturally supports undo operations, which is an important application scenario. To implement undo functionality, command objects need to store sufficient state information to roll back operations.

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(`Set value to: ${this.value}`);
  }
  
  undo() {
    this.receiver.value = this.previousValue;
    console.log(`Undo to: ${this.previousValue}`);
  }
}

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

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

undoableCommand.execute(); // Set value to: 10
console.log(receiverWithState.value); // 10
undoableCommand.undo();    // Undo to: 0
console.log(receiverWithState.value); // 0

More Complex Undo Stack Implementation

In practical applications, we often need to maintain a complete undo stack to support multi-step undo and redo:

class CommandManager {
  constructor() {
    this.undoStack = [];
    this.redoStack = [];
  }
  
  execute(command) {
    command.execute();
    this.undoStack.push(command);
    this.redoStack = []; // Clear the redo stack when executing new commands
  }
  
  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);
    }
  }
}

// Usage Example
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

Application of the Command Pattern in UI Interactions

The Command pattern is particularly suitable for handling user interactions in front-end development, such as button clicks and menu selections. Below is a practical DOM operation example:

// DOM Operation Command
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;
  }
}

// Usage Example
const button = document.createElement('button');
button.textContent = 'Click Me';
document.body.appendChild(button);

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

// Execute Commands
commandManager.execute(changeColorCmd);
commandManager.execute(changeTextCmd);

// Add Undo Button
const undoBtn = document.createElement('button');
undoBtn.textContent = 'Undo';
undoBtn.addEventListener('click', () => commandManager.undo());
document.body.appendChild(undoBtn);

// Add Redo Button
const redoBtn = document.createElement('button');
redoBtn.textContent = 'Redo';
redoBtn.addEventListener('click', () => commandManager.redo());
document.body.appendChild(redoBtn);

Command Queue and Delayed Execution

The Command pattern also supports placing commands in a queue for on-demand or delayed execution, which is useful in scenarios like animations and batch operations:

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();
      
      // Simulate completion of asynchronous operation
      setTimeout(() => {
        this.isExecuting = false;
        this.executeNext();
      }, 1000);
    }
  }
}

// Usage Example
const queue = new CommandQueue();
const receiver = {
  action: (msg) => console.log(`Execute: ${msg}`)
};

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

queue.add(new LogCommand(receiver, 'First Command'));
queue.add(new LogCommand(receiver, 'Second Command'));
queue.add(new LogCommand(receiver, 'Third Command'));

// Output:
// Execute: First Command
// (After 1 second) Execute: Second Command
// (After another second) Execute: Third Command

Variants and Application Scenarios of the Command Pattern

The Command pattern has several variants, suitable for different scenarios:

  1. Simple Command: Basic command containing only an execute method.
  2. Undoable Command: Command with an undo method.
  3. Transactional Command: Commands that either all succeed or all roll back.
  4. Macro Command: Combines multiple commands into a single command for execution.

Typical application scenarios include:

  • GUI buttons and menu items
  • Transaction processing systems
  • Progress bar operations
  • Multi-level undo/redo functionality
  • Logging systems
  • Task scheduling systems
// Transactional Command Example
class Transaction {
  constructor() {
    this.commands = [];
    this.executed = false;
  }
  
  add(command) {
    if (this.executed) {
      throw new Error('Transaction already executed, cannot add new commands');
    }
    this.commands.push(command);
  }
  
  execute() {
    if (this.executed) return;
    
    try {
      this.commands.forEach(cmd => cmd.execute());
      this.executed = true;
    } catch (error) {
      // Roll back executed commands if execution fails
      for (let i = this.commands.length - 1; i >= 0; i--) {
        if (this.commands[i].undo) {
          this.commands[i].undo();
        }
      }
      throw error;
    }
  }
}

// Usage Example
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('Transaction failed:', error);
}

Command Pattern and Memory Management

When implementing undo/redo functionality with the Command pattern, memory management issues must be considered. Long-running applications may accumulate a large number of command objects, leading to high memory usage. Solutions include:

  1. Limiting History Length: Retain only the most recent N commands.
  2. Snapshot Pattern: Periodically save complete states instead of recording every command.
  3. Command Compression: Merge multiple consecutive commands into one.
// Command Manager with Limited History Length
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(); // Remove the oldest command
    }
  }
}

// Snapshot Command Example
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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.