Command Pattern
Definitionβ
The Command encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
Coffee Shop Exampleβ
Baristas take coffee orders as command objects. Each command can be executed (make the coffee), undone (cancel the order), and stored in a history for undo/redo. Orders can also be queued during rush hour.
Structureβ
Command Interfaceβ
public interface ICommand
{
void Execute();
void Undo();
string GetDescription();
}
Receiver β The Coffee Shopβ
public record Beverage(string Coffee, CupSize Size, decimal Price)
{
public List<string> Toppings { get; init; } = new();
public override string ToString()
{
var base_ = $"{Size} {Coffee} β ${Price:F2}";
return Toppings.Count > 0 ? $"{base_} + [{string.Join(", ", Toppings)}]" : base_;
}
}
public class CoffeeShop
{
private readonly List<Beverage> _completedOrders = new();
public Beverage MakeCoffee(string coffee, CupSize size, decimal price)
{
var beverage = new Beverage(coffee, size, price);
_completedOrders.Add(beverage);
Console.WriteLine($" β Made: {beverage}");
return beverage;
}
public void CancelOrder(Beverage beverage)
{
_completedOrders.Remove(beverage);
Console.WriteLine($" β Cancelled: {beverage.Coffee}");
}
public void AddTopping(Beverage beverage, string topping)
{
beverage.Toppings.Add(topping);
Console.WriteLine($" β Added {topping} to {beverage.Coffee}");
}
public void RemoveTopping(Beverage beverage, string topping)
{
beverage.Toppings.Remove(topping);
Console.WriteLine($" β Removed {topping} from {beverage.Coffee}");
}
}
Concrete Commandsβ
public class OrderCoffeeCommand : ICommand
{
private readonly CoffeeShop _shop;
private readonly string _coffee;
private readonly CupSize _size;
private readonly decimal _price;
private Beverage? _lastBeverage;
public OrderCoffeeCommand(CoffeeShop shop, string coffee, CupSize size, decimal price)
{
_shop = shop;
_coffee = coffee;
_size = size;
_price = price;
}
public void Execute()
{
_lastBeverage = _shop.MakeCoffee(_coffee, _size, _price);
}
public void Undo()
{
if (_lastBeverage is not null)
_shop.CancelOrder(_lastBeverage);
}
public string GetDescription() => $"Order {_size} {_coffee}";
}
public class AddToppingCommand : ICommand
{
private readonly CoffeeShop _shop;
private readonly Beverage _beverage;
private readonly string _topping;
public AddToppingCommand(CoffeeShop shop, Beverage beverage, string topping)
{
_shop = shop;
_beverage = beverage;
_topping = topping;
}
public void Execute() => _shop.AddTopping(_beverage, _topping);
public void Undo() => _shop.RemoveTopping(_beverage, _topping);
public string GetDescription() => $"Add {_topping}";
}
Invoker β The Barista (with Undo/Redo)β
public class Barista
{
private readonly Stack<ICommand> _history = new();
private readonly Stack<ICommand> _redoStack = new();
public void ExecuteCommand(ICommand command)
{
command.Execute();
_history.Push(command);
_redoStack.Clear(); // New command clears redo history
}
public void Undo()
{
if (_history.Count == 0) return;
var command = _history.Pop();
command.Undo();
_redoStack.Push(command);
Console.WriteLine($" β©οΈ Undo: {command.GetDescription()}");
}
public void Redo()
{
if (_redoStack.Count == 0) return;
var command = _redoStack.Pop();
command.Execute();
_history.Push(command);
Console.WriteLine($" βͺοΈ Redo: {command.GetDescription()}");
}
public void PrintHistory()
{
Console.WriteLine(" History:");
foreach (var cmd in _history)
Console.WriteLine($" - {cmd.GetDescription()}");
}
}
Clientβ
var shop = new CoffeeShop();
var barista = new Barista();
// Execute commands
barista.ExecuteCommand(new OrderCoffeeCommand(shop, "Latte", CupSize.Large, 5.00m));
// β Made: Large Latte β $5.00
barista.ExecuteCommand(new OrderCoffeeCommand(shop, "Espresso", CupSize.Small, 2.50m));
// β Made: Small Espresso β $2.50
// Undo the espresso order
barista.Undo();
// β Cancelled: Espresso
// β©οΈ Undo: Order Small Espresso
// Redo β bring the espresso back
barista.Redo();
// β Made: Small Espresso β $2.50
// βͺοΈ Redo: Order Small Espresso
barista.PrintHistory();
// History:
// - Order Small Espresso
// - Order Large Latte
Each action is a command object β it captures the request, the receiver, and knows how to undo itself. This enables undo/redo, command queuing, and logging without modifying the receiver.
Command Typesβ
| Type | Purpose | Example |
|---|---|---|
| Simple Command | One action | OrderCoffeeCommand |
| Undoable Command | Execute + undo | AddToppingCommand |
| Macro Command | Batch of commands | "Breakfast Combo" = Latte + Croissant + Juice |
| Queued Command | Deferred execution | Orders during rush hour |
.NET Real-World Usageβ
IAsyncResult/ Tasks β commands as asynchronous operations- WPF
ICommandβ MVVM command binding for buttons - ASP.NET Core middleware β each middleware is a command in a pipeline
- CQRS β Command Query Responsibility Segregation separates commands from queries
System.Transactionβ rollback support via compensating actions
When to Useβ
- You need to undo/redo operations
- You want to queue, schedule, or log requests
- You need to parameterize objects with operations
- You want to implement macro commands (batch operations)
When NOT to Useβ
- Operations are simple and don't need undo β direct method calls suffice
- There's no need for queuing, logging, or undo
- The overhead of command objects isn't justified
Key Takeawaysβ
- Command encapsulates a request as an object with
Execute()andUndo() - Decouples the invoker (barista) from the receiver (coffee shop)
- Enables undo/redo, queuing, logging, and macro operations
- Each command is self-contained β it knows the receiver and the action
Interview Questionsβ
Q: How does Command enable undo/redo?
Each command stores the state needed to reverse its effect. Undo() applies the inverse operation. The invoker maintains a stack β pop to undo, push to a redo stack, pop from redo stack to redo.
Q: How is Command different from Strategy? Command encapsulates a request (action to perform). Strategy encapsulates an algorithm (how to compute). Commands are often one-shot and undoable; strategies are long-lived and swappable.
Q: What's CQRS? Command Query Responsibility Segregation separates read operations (queries) from write operations (commands). Commands change state and return nothing. Queries return data and don't change state. This pattern scales reads and writes independently.
Related Topicsβ
- Strategy β encapsulates algorithms, not requests
- Chain of Responsibility β passes request along a chain
- Memento β captures state for undo (complementary to Command)
- SOLID β SRP β each command has a single responsibility