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
- Command: An interface or abstract class defining a method (usually execute()) to perform an action, and optionally undo() for reversing it.
- Concrete Command: A class implementing the Command interface, specifying the receiver and the action to perform.
- Receiver: The object that performs the actual work when the command is executed.
- Invoker: The object that holds and triggers the command, calling its execute() method.
- 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.