Command Pattern

The Command Pattern is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation allows for the parameterization of methods with different requests, the queuing or logging of requests, and the support for undoable operations.

The Command Pattern is a behavioral design pattern that encapsulates a request as an object, thereby allowing parameterization of clients with different requests, queuing of requests, and support for undoable operations. It decouples the sender (invoker) of a request from the receiver that performs the action, enabling flexible and extensible command execution.

Important Components

  1. Command: An interface or abstract class defining a method (usually execute()) to perform an action, and optionally undo() for reversing it.
  2. Concrete Command: A class implementing the Command interface, specifying the receiver and the action to perform.
  3. Receiver: The object that performs the actual work when the command is executed.
  4. Invoker: The object that holds and triggers the command, calling its execute() method.
  5. Client: Creates the command objects, associates them with receivers, and sets them on the invoker.

How It Works

  • The Client creates a Concrete Command, binding it to a Receiver and specifying the action.
  • The Invoker stores the command and calls its execute() method when triggered.
  • The Concrete Command delegates the request to the Receiver to perform the action.
  • Optionally, the command can implement undo() to reverse the action.
  • This setup allows commands to be queued, logged, or undone, and supports dynamic assignment of commands to invokers.

Sample Implementation

Let’s model a text editor where commands like WriteText and EraseText can be executed and undone.

// Command Interface
interface Command {
    void execute();
    void undo();
}

// Receiver
class TextEditor {
    private StringBuilder text = new StringBuilder();

    public void write(String text) {
        this.text.append(text);
        System.out.println("Wrote: " + text + " | Current text: " + this.text);
    }

    public void erase(int length) {
        if (length > text.length()) length = text.length();
        String erased = text.substring(text.length() - length);
        text.delete(text.length() - length, text.length());
        System.out.println("Erased: " + erased + " | Current text: " + this.text);
    }

    public String getText() {
        return text.toString();
    }
}

// Concrete Commands
class WriteCommand implements Command {
    private TextEditor editor;
    private String text;

    public WriteCommand(TextEditor editor, String text) {
        this.editor = editor;
        this.text = text;
    }

    @Override
    public void execute() {
        editor.write(text);
    }

    @Override
    public void undo() {
        editor.erase(text.length());
    }
}

class EraseCommand implements Command {
    private TextEditor editor;
    private int length;
    private String erasedText;

    public EraseCommand(TextEditor editor, int length) {
        this.editor = editor;
        this.length = length;
    }

    @Override
    public void execute() {
        String currentText = editor.getText();
        int actualLength = Math.min(length, currentText.length());
        erasedText = currentText.substring(currentText.length() - actualLength);
        editor.erase(length);
    }

    @Override
    public void undo() {
        editor.write(erasedText);
    }
}

// Invoker
class TextEditorInvoker {
    private List<Command> commandHistory = new ArrayList<>();

    public void executeCommand(Command command) {
        command.execute();
        commandHistory.add(command);
    }

    public void undoLastCommand() {
        if (!commandHistory.isEmpty()) {
            Command lastCommand = commandHistory.remove(commandHistory.size() - 1);
            lastCommand.undo();
        } else {
            System.out.println("Nothing to undo.");
        }
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        // Create receiver
        TextEditor editor = new TextEditor();

        // Create invoker
        TextEditorInvoker invoker = new TextEditorInvoker();

        // Create commands
        Command writeHello = new WriteCommand(editor, "Hello");
        Command writeWorld = new WriteCommand(editor, " World");
        Command erase5 = new EraseCommand(editor, 5);

        // Execute commands
        System.out.println("Executing commands:");
        invoker.executeCommand(writeHello);
        invoker.executeCommand(writeWorld);
        invoker.executeCommand(erase5);

        // Undo commands
        System.out.println("\nUndoing commands:");
        invoker.undoLastCommand();
        invoker.undoLastCommand();
        invoker.undoLastCommand();
    }
}
/*
Executing commands:
Wrote: Hello | Current text: Hello
Wrote: World | Current text: Hello World
Erased: World | Current text: Hello

Undoing commands:
Wrote: World | Current text: Hello World
Erased: World | Current text: Hello
Wrote: Hello | Current text: Hello
*/Code language: JavaScript (javascript)

Use case and Implementation

 Implementing transaction management, where each transaction is a command that can be execute and undo.

//CommandPatternDemo.java
import java.util.*;
// BankAccount class to manage balance
class BankAccount {
    private int balance;

    public BankAccount(int initialBalance) {
        this.balance = initialBalance;
    }

    public void deposit(int amount) {
        balance += amount;
    }

    public void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
        } else {
            System.out.println("Insufficient balance for withdrawal of: " + amount);
        }
    }

    public int getBalance() {
        return balance;
    }
}

// Command Interface
interface Command {
    void execute();
    void undo();
}

// Concrete Command: Transaction
class Transaction implements Command {
    private BankAccount account;
    private String operation;
    private int amount;

    public Transaction(BankAccount account, String operation, int amount) {
        this.account = account;
        this.operation = operation;
        this.amount = amount;
    }

    @Override
    public void execute() {
        if ("Deposit".equals(operation)) {
            account.deposit(amount);
            System.out.println("Deposited: " + amount + ", Balance: " + account.getBalance());
        } else if ("Withdraw".equals(operation)) {
            account.withdraw(amount);
            System.out.println("Withdrew: " + amount + ", Balance: " + account.getBalance());
        }
    }

    @Override
    public void undo() {
        if ("Deposit".equals(operation)) {
            account.withdraw(amount); // Undo deposit by withdrawing the amount
            System.out.println("Undoing deposit of: " + amount + ", Balance: " + account.getBalance());
        } else if ("Withdraw".equals(operation)) {
            account.deposit(amount); // Undo withdraw by depositing the amount back
            System.out.println("Undoing withdrawal of: " + amount + ", Balance: " + account.getBalance());
        }
    }
}

// Invoker: TransactionManager
class TransactionManager {
    private List<Command> transactions = new ArrayList<>();

    public void addTransaction(Command transaction) {
        transactions.add(transaction);
    }

    public void executeTransactions() {
        for (Command transaction : transactions) {
            transaction.execute();
        }
    }

    public void undoLastTransaction() {
        if (!transactions.isEmpty()) {
            Command lastTransaction = transactions.remove(transactions.size() - 1);
            lastTransaction.undo();
        } else {
            System.out.println("No transactions to undo.");
        }
    }
}

// Client
public class CommandPatternDemo {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(1000); // Initial balance of 1000
        TransactionManager manager = new TransactionManager();

        System.out.println("Initial Balance: " + account.getBalance());

        // Adding transactions
        manager.addTransaction(new Transaction(account, "Deposit", 1000));
        manager.addTransaction(new Transaction(account, "Withdraw", 500));

        // Executing transactions
        System.out.println("\nExecuting Transactions:");
        manager.executeTransactions();

        // Balance after transactions
        System.out.println("\nBalance After Transactions: " + account.getBalance());

        // Undoing the last transaction
        System.out.println("\nUndoing Last Transaction:");
        manager.undoLastTransaction();

        // Balance after undoing
        System.out.println("\nBalance After Undo: " + account.getBalance());
    }
}

/*
C:\>javac CommandPatternDemo.java

C:\>java CommandPatternDemo
Initial Balance: 1000

Executing Transactions:
Deposited: 1000, Balance: 2000
Withdrew: 500, Balance: 1500

Balance After Transactions: 1500

Undoing Last Transaction:
Undoing withdrawal of: 500, Balance: 2000

Balance After Undo: 2000
*/

Advantages

  • Decoupling: Separates the invoker from the receiver, allowing independent changes.
  • Flexibility: Commands can be parameterized, queued, or extended with new types.
  • Undo/Redo: Easily supports reversible operations by storing command history.
  • Extensibility: New commands can be added without modifying existing code.
  • Logging/Queuing: Commands can be logged or queued for later execution.

Disadvantages

  • Complexity: Introduces additional classes, increasing code complexity.
  • Memory Overhead: Storing command history for undo/redo can consume significant memory.
  • Maintenance: Managing a large number of command classes can be cumbersome.

When to Use

  • When you need to encapsulate requests as objects for parameterization or queuing.
  • When you want to support undo/redo functionality.
  • When you need to decouple the sender of a request from the object that performs it.
  • When you want to log, schedule, or prioritize requests (e.g., in a transaction system).
  • When you need to support macros or composite commands.

Real-World Example

  • Text Editors: Commands like cut, paste, or format in editors like Microsoft Word support execution and undo.
  • GUI Frameworks: Button clicks trigger commands (e.g., save, open) in Java Swing or WPF.
  • Transaction Systems: Database operations are encapsulated as commands for rollback capabilities.
  • Game Development: Player actions (move, attack) are commands that can be undone or replayed.
  • The Command Pattern provides a robust mechanism for decoupling the sender and receiver of requests, enabling flexible and maintainable code. It supports undoable operations, command queuing, and macro commands, making it a powerful tool for a wide range of applications. By encapsulating requests as objects, the Command Pattern promotes a clean separation of concerns and enhances the extensibility of software systems.
Scroll to Top