Skip to main content

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
tip

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​

TypePurposeExample
Simple CommandOne actionOrderCoffeeCommand
Undoable CommandExecute + undoAddToppingCommand
Macro CommandBatch of commands"Breakfast Combo" = Latte + Croissant + Juice
Queued CommandDeferred executionOrders 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() and Undo()
  • 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.