Files

11 KiB

Command Pattern

Problem

A text editor needs to support typing, deleting, and undo/redo operations. If the editor directly performs each operation inline, there's no way to reverse actions, queue them, or log them for replay. Adding undo requires storing inverse operations, which becomes fragile when tightly coupled to the editor logic.

Solution

Encapsulate each operation as a Command object with execute() and undo() methods. The editor (Invoker) stores a history of executed commands and can walk backward (undo) or forward (redo) through that history. The command object holds all the information needed to perform — and reverse — an action.

Key participants:

  • Command — interface with execute() and undo()
  • ConcreteCommand — implements a specific operation (TypeCommand, DeleteCommand)
  • Receiver — the object being operated on (the text buffer)
  • Invoker — stores history and triggers execute/undo/redo

When to Use

  • You need undo/redo functionality
  • Operations should be queued, logged, or scheduled for later execution
  • You want to decouple the object that invokes an operation from the one that performs it
  • You need to support transactional behavior (execute all or rollback)

When to Avoid

  • Operations are simple and don't need undo — adds unnecessary complexity
  • The number of command types is huge and each is trivial
  • Performance is critical and command object allocation is a bottleneck

Pseudocode

INTERFACE Command:
    method execute()
    method undo()

CLASS TypeCommand IMPLEMENTS Command:
    receiver, text
    method execute():
        receiver.insert(text)
    method undo():
        receiver.delete_last(len(text))

CLASS Editor:
    history = []
    redo_stack = []

    method execute_command(cmd):
        cmd.execute()
        history.push(cmd)
        redo_stack.clear()

    method undo():
        cmd = history.pop()
        cmd.undo()
        redo_stack.push(cmd)

    method redo():
        cmd = redo_stack.pop()
        cmd.execute()
        history.push(cmd)

Python

from __future__ import annotations
from abc import ABC, abstractmethod


class TextBuffer:
    def __init__(self) -> None:
        self.content = ""

    def insert(self, text: str) -> None:
        self.content += text

    def delete_last(self, count: int) -> str:
        removed = self.content[-count:]
        self.content = self.content[:-count]
        return removed

    def __str__(self) -> str:
        return self.content


class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass

    @abstractmethod
    def undo(self) -> None:
        pass


class TypeCommand(Command):
    def __init__(self, buffer: TextBuffer, text: str) -> None:
        self.buffer = buffer
        self.text = text

    def execute(self) -> None:
        self.buffer.insert(self.text)

    def undo(self) -> None:
        self.buffer.delete_last(len(self.text))


class DeleteCommand(Command):
    def __init__(self, buffer: TextBuffer, count: int) -> None:
        self.buffer = buffer
        self.count = count
        self.deleted = ""

    def execute(self) -> None:
        self.deleted = self.buffer.delete_last(self.count)

    def undo(self) -> None:
        self.buffer.insert(self.deleted)


class Editor:
    def __init__(self) -> None:
        self.buffer = TextBuffer()
        self.history: list[Command] = []
        self.redo_stack: list[Command] = []

    def execute(self, cmd: Command) -> None:
        cmd.execute()
        self.history.append(cmd)
        self.redo_stack.clear()

    def undo(self) -> None:
        if not self.history:
            print("  Nothing to undo")
            return
        cmd = self.history.pop()
        cmd.undo()
        self.redo_stack.append(cmd)

    def redo(self) -> None:
        if not self.redo_stack:
            print("  Nothing to redo")
            return
        cmd = self.redo_stack.pop()
        cmd.execute()
        self.history.append(cmd)


def main() -> None:
    editor = Editor()

    editor.execute(TypeCommand(editor.buffer, "Hello"))
    print(f"After type 'Hello': '{editor.buffer}'")

    editor.execute(TypeCommand(editor.buffer, " World"))
    print(f"After type ' World': '{editor.buffer}'")

    editor.execute(DeleteCommand(editor.buffer, 6))
    print(f"After delete 6 chars: '{editor.buffer}'")

    editor.undo()
    print(f"After undo (restore delete): '{editor.buffer}'")

    editor.undo()
    print(f"After undo (restore ' World'): '{editor.buffer}'")

    editor.redo()
    print(f"After redo (re-type ' World'): '{editor.buffer}'")

    editor.undo()
    print(f"After undo again: '{editor.buffer}'")

    editor.undo()
    print(f"After undo (restore 'Hello'): '{editor.buffer}'")


if __name__ == "__main__":
    main()

Output:

After type 'Hello': 'Hello'
After type ' World': 'Hello World'
After delete 6 chars: 'Hello'
After undo (restore delete): 'Hello World'
After undo (restore ' World'): 'Hello'
After redo (re-type ' World'): 'Hello World'
After undo again: 'Hello'
After undo (restore 'Hello'): ''

Go

package main

import "fmt"

type TextBuffer struct {
	content string
}

func (b *TextBuffer) Insert(text string) {
	b.content += text
}

func (b *TextBuffer) DeleteLast(count int) string {
	if count > len(b.content) {
		count = len(b.content)
	}
	removed := b.content[len(b.content)-count:]
	b.content = b.content[:len(b.content)-count]
	return removed
}

func (b *TextBuffer) String() string {
	return b.content
}

type Command interface {
	Execute()
	Undo()
}

type TypeCommand struct {
	buffer *TextBuffer
	text   string
}

func (c *TypeCommand) Execute() { c.buffer.Insert(c.text) }
func (c *TypeCommand) Undo()    { c.buffer.DeleteLast(len(c.text)) }

type DeleteCommand struct {
	buffer  *TextBuffer
	count   int
	deleted string
}

func (c *DeleteCommand) Execute() {
	c.deleted = c.buffer.DeleteLast(c.count)
}

func (c *DeleteCommand) Undo() {
	c.buffer.Insert(c.deleted)
}

type Editor struct {
	Buffer    *TextBuffer
	history   []Command
	redoStack []Command
}

func NewEditor() *Editor {
	return &Editor{Buffer: &TextBuffer{}}
}

func (e *Editor) Execute(cmd Command) {
	cmd.Execute()
	e.history = append(e.history, cmd)
	e.redoStack = nil
}

func (e *Editor) Undo() {
	if len(e.history) == 0 {
		fmt.Println("  Nothing to undo")
		return
	}
	cmd := e.history[len(e.history)-1]
	e.history = e.history[:len(e.history)-1]
	cmd.Undo()
	e.redoStack = append(e.redoStack, cmd)
}

func (e *Editor) Redo() {
	if len(e.redoStack) == 0 {
		fmt.Println("  Nothing to redo")
		return
	}
	cmd := e.redoStack[len(e.redoStack)-1]
	e.redoStack = e.redoStack[:len(e.redoStack)-1]
	cmd.Execute()
	e.history = append(e.history, cmd)
}

func main() {
	editor := NewEditor()

	editor.Execute(&TypeCommand{buffer: editor.Buffer, text: "Hello"})
	fmt.Printf("After type 'Hello': '%s'\n", editor.Buffer)

	editor.Execute(&TypeCommand{buffer: editor.Buffer, text: " World"})
	fmt.Printf("After type ' World': '%s'\n", editor.Buffer)

	editor.Execute(&DeleteCommand{buffer: editor.Buffer, count: 6})
	fmt.Printf("After delete 6 chars: '%s'\n", editor.Buffer)

	editor.Undo()
	fmt.Printf("After undo (restore delete): '%s'\n", editor.Buffer)

	editor.Undo()
	fmt.Printf("After undo (restore ' World'): '%s'\n", editor.Buffer)

	editor.Redo()
	fmt.Printf("After redo (re-type ' World'): '%s'\n", editor.Buffer)

	editor.Undo()
	fmt.Printf("After undo again: '%s'\n", editor.Buffer)

	editor.Undo()
	fmt.Printf("After undo (restore 'Hello'): '%s'\n", editor.Buffer)
}

Output:

After type 'Hello': 'Hello'
After type ' World': 'Hello World'
After delete 6 chars: 'Hello'
After undo (restore delete): 'Hello World'
After undo (restore ' World'): 'Hello'
After redo (re-type ' World'): 'Hello World'
After undo again: 'Hello'
After undo (restore 'Hello'): ''

JavaScript

class TextBuffer {
  constructor() {
    this.content = "";
  }

  insert(text) {
    this.content += text;
  }

  deleteLast(count) {
    const removed = this.content.slice(-count);
    this.content = this.content.slice(0, -count);
    return removed;
  }

  toString() {
    return this.content;
  }
}

class TypeCommand {
  constructor(buffer, text) {
    this.buffer = buffer;
    this.text = text;
  }

  execute() {
    this.buffer.insert(this.text);
  }

  undo() {
    this.buffer.deleteLast(this.text.length);
  }
}

class DeleteCommand {
  constructor(buffer, count) {
    this.buffer = buffer;
    this.count = count;
    this.deleted = "";
  }

  execute() {
    this.deleted = this.buffer.deleteLast(this.count);
  }

  undo() {
    this.buffer.insert(this.deleted);
  }
}

class Editor {
  constructor() {
    this.buffer = new TextBuffer();
    this.history = [];
    this.redoStack = [];
  }

  execute(cmd) {
    cmd.execute();
    this.history.push(cmd);
    this.redoStack = [];
  }

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

  redo() {
    if (this.redoStack.length === 0) {
      console.log("  Nothing to redo");
      return;
    }
    const cmd = this.redoStack.pop();
    cmd.execute();
    this.history.push(cmd);
  }
}

// --- Demo ---
const editor = new Editor();

editor.execute(new TypeCommand(editor.buffer, "Hello"));
console.log(`After type 'Hello': '${editor.buffer}'`);

editor.execute(new TypeCommand(editor.buffer, " World"));
console.log(`After type ' World': '${editor.buffer}'`);

editor.execute(new DeleteCommand(editor.buffer, 6));
console.log(`After delete 6 chars: '${editor.buffer}'`);

editor.undo();
console.log(`After undo (restore delete): '${editor.buffer}'`);

editor.undo();
console.log(`After undo (restore ' World'): '${editor.buffer}'`);

editor.redo();
console.log(`After redo (re-type ' World'): '${editor.buffer}'`);

editor.undo();
console.log(`After undo again: '${editor.buffer}'`);

editor.undo();
console.log(`After undo (restore 'Hello'): '${editor.buffer}'`);

Output:

After type 'Hello': 'Hello'
After type ' World': 'Hello World'
After delete 6 chars: 'Hello'
After undo (restore delete): 'Hello World'
After undo (restore ' World'): 'Hello'
After redo (re-type ' World'): 'Hello World'
After undo again: 'Hello'
After undo (restore 'Hello'): ''
  • Memento — Command stores what action was taken; Memento stores the state before the action. Together they enable undo/redo.
  • Strategy — Both encapsulate behavior as objects. Commands are action-oriented (do/undo); Strategies are algorithm-oriented (how to do something).
  • Chain of Responsibility — Commands can be passed along a chain; each handler decides to execute or forward.
  • Composite — Macro commands are Composite Commands: a composite that executes a sequence of child commands atomically.
  • Observer — Commands are often triggered by Observer events and can emit events on completion.
  • Message Queue — Commands are natural messages; queuing them decouples invocation from execution.