Skip to Content
Course content

161: The Command Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why can't I just call a function instead of wrapping it in an object?

That's the first thing everyone asks. If you just want to make text bold, why not just call makeBold()? The problem is that a function call is an immediate action. Once it's gone, it's gone. You have no record of it, and you certainly can't "un-call" it.

By turning that request into a Command object, you're essentially turning a verb into a noun. Instead of "doing the thing," you're "creating a thing that knows how to do the thing." This decoupling is huge. It means the piece of code triggering the action (like a button in your UI) doesn't need to know anything about the actual logic of the operation; it just needs to know that the object it's holding has an execute() method.

What does the actual structure look like in JavaScript?

In a language like Java, you'd be dealing with strict interfaces. In JavaScript, we just rely on a consistent shape. Every command object should have a standard method—usually execute()—and often an undo() method if you're tracking history.

Let's imagine we're building a simple text editor. Here is how I'd set up a command to handle changing the text color:

class ChangeColorCommand {
  constructor(editor, newColor) {
    this.editor = editor;
    this.newColor = newColor;
    this.oldColor = null; // We'll save this during execution
  }

  execute() {
    // Save the current state so we can revert later
    this.oldColor = this.editor.color;
    this.editor.color = this.newColor;
    console.log(`Color changed to ${this.newColor}`);
  }

  undo() {
    this.editor.color = this.oldColor;
    console.log(`Color reverted to ${this.oldColor}`);
  }
}

// The "Receiver" - the object actually being manipulated
const myEditor = { color: 'black' };

// The "Client" creates the command
const redCommand = new ChangeColorCommand(myEditor, 'red');

// The "Invoker" (like a button click) executes it
redCommand.execute(); // myEditor.color is now 'red'

How do I use this to actually implement an Undo feature?

This is where the Command pattern really shines. Since each command object stores its own previous state (like oldColor in the example above), you can just push every command you execute onto a stack. When the user hits Ctrl+Z, you pop the last command off the stack and call its undo() method.

I usually implement this with a "Command Manager" or "Invoker" class to keep the history clean. Here's a quick look at how that logic flows:

class CommandManager {
  constructor() {
    this.history = [];
  }

  execute(command) {
    command.execute();
    this.history.push(command);
  }

  undo() {
    if (this.history.length === 0) {
      console.log("Nothing to undo!");
      return;
    }
    const command = this.history.pop();
    command.undo();
  }
}

const manager = new CommandManager();
const redCmd = new ChangeColorCommand(myEditor, 'red');
const blueCmd = new ChangeColorCommand(myEditor, 'blue');

manager.execute(redCmd);  // Color: red
manager.execute(blueCmd); // Color: blue
manager.undo();           // Color: red (blueCmd popped and undone)
manager.undo();           // Color: black (redCmd popped and undone)

It's a simple pattern, but it's the backbone of almost every professional software tool that has a history panel or an undo system. You're not saving the entire state of the application every time (which would be a memory nightmare); you're just saving the specific delta needed to reverse a single action.




📋 Practical Task

Exercise: Building a Command-Based Volume Controller

You are building a volume control system for a media player. Instead of just changing a variable, you need to implement it using the Command Pattern so that users can undo their volume changes.

  • Create a VolumeReceiver object that has a volume property (starting at 50) and a method to log the current volume to the console.
  • Create a ChangeVolumeCommand class. It should take the receiver and a newVolume value in the constructor. It must implement execute()` and undo().
  • Create a RemoteControl class (the Invoker) that maintains a history array. It should have an pressButton(command) method to execute and store commands, and an undoPress() method to revert the last action.
  • Test your implementation: Change the volume to 80, then to 20, then call undoPress() twice to ensure the volume returns to 50.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.