Today, we’ll dive into another behavioral design pattern: the Command Pattern. I’ll explain what it is, demonstrate its implementation with a simple example, and discuss its impact on performance. By the end of this article, you’ll understand why this pattern is a powerful tool for building flexible and extensible systems.
What is the Command Pattern?
The Command Pattern is a behavioral design pattern that encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. The core idea is to decouple the object that invokes an operation from the one that knows how to perform it.
The pattern consists of the following key components:
- Client: Creates and configures the concrete command objects.
- Invoker: Holds a command and triggers its execution.
- Receiver: Knows how to perform the actual action.
- Command: An interface or abstract class that defines the execution method.
- Concrete Command: Implements the Command interface and binds the action to the receiver.
Implementing the Command Pattern
Let’s explore the Command Pattern with a practical example: a light remote control. In this scenario, we’ll create commands to turn a light on and off, and we’ll also implement an undo feature.
Step 1: Define the Command Interface
We start by defining an interface ICommand
with two methods: Execute()
and Undo()
.
public interface ICommand
{
void Execute();
void Undo();
}
Step 2: Create Concrete Commands
Next, we implement two concrete commands: LightOnCommand and LightOffCommand. These commands encapsulate the actions of turning the light on and off.
public class LightOnCommand(Light light) : ICommand
{
public void Execute()
{
light.TurnOn();
}
public void Undo()
{
light.TurnOff();
}
}
public class LightOffCommand(Light light) : ICommand
{
public void Execute()
{
light.TurnOff();
}
public void Undo()
{
light.TurnOn();
}
}
Step 3: Implement the Receiver
The Light class acts as the receiver, which knows how to perform the actual actions.
public class Light
{
public void TurnOn()
{
Console.WriteLine("The light is ON");
}
public void TurnOff()
{
Console.WriteLine("The light is OFF");
}
}
Step 4: Create the Invoker
The CommandRemoteControl class acts as the invoker. It holds a command and triggers its execution when needed.
public class CommandRemoteControl
{
private ICommand? _command;
public void Invoke(ICommand? command)
{
_command = command;
}
public void PressButton()
{
if (_command != null)
{
_command.Execute();
}
else
{
Console.WriteLine("No command set.");
}
}
public void PressUndo()
{
if (_command != null)
{
_command.Undo();
}
else
{
Console.WriteLine("No command set to undo.");
}
}
}
Step 5: Simulate User Interaction
To demonstrate how the pattern works, we’ll simulate user interaction with a method that calls different commands based on user choices.
public static void RunCommand()
{
Light light = new Light();
CommandRemoteControl commandRemote = new CommandRemoteControl();
ICommand lightOnCommand = new LightOnCommand(light);
ICommand lightOffCommand = new LightOffCommand(light);
ICommand? lastCommand = null;
int[] choices = { 1, 2, 3, 0 };
Console.WriteLine("Choose light action: 1. On, 2. Off, 3. Undo");
foreach (var choice in choices)
{
switch (choice)
{
case 1:
Console.WriteLine($"Chosen action: On");
commandRemote.Invoke(lightOnCommand);
lastCommand = lightOnCommand;
commandRemote.PressButton();
break;
case 2:
Console.WriteLine($"Chosen action: Off");
commandRemote.Invoke(lightOffCommand);
lastCommand = lightOffCommand;
commandRemote.PressButton();
break;
case 3:
Console.WriteLine($"Chosen action: Undo");
commandRemote.PressUndo();
commandRemote.Invoke(lastCommand);
break;
default:
Console.WriteLine("Chosen action: Unknown");
commandRemote.Invoke(null);
commandRemote.PressButton();
break;
}
}
}
When you run this code, you’ll see the following output:
I needed four classes to implement this pattern. You might need more classes if you have something more complicated, like a light switcher with many commands. This will increase the complexity of the entire application.
Breaking the Rules: A Simplified Approach
While the Command Pattern provides flexibility, it’s tempting to simplify the code by breaking the pattern. Let’s explore a non-command approach where we directly pass commands as integers.
public class NonCommandRemoteControl(Light? light)
{
public void PressButton(int command)
{
switch (command)
{
case 1:
light?.TurnOn();
break;
case 2:
light?.TurnOff();
break;
default:
Console.WriteLine("Incorrect Command.");
break;
}
}
public void PressUndo(int lastCommand)
{
switch (lastCommand)
{
case 1:
light?.TurnOff();
break;
case 2:
light?.TurnOn();
break;
default:
Console.WriteLine("No previous action to undo.");
break;
}
}
To simulate user interaction, we use a similar method:
public static void RunNonCommand()
{
Light light = new Light();
NonCommandRemoteControl nonCommandRemote = new NonCommandRemoteControl(light);
int lastCommand = 0;
int[] choices = { 1, 2, 3, 0 };
Console.WriteLine("Choose light action: 1. On, 2. Off, 3. Undo");
foreach (var choice in choices)
{
switch (choice)
{
case 1:
Console.WriteLine($"Chosen action: On");
lastCommand = choice;
nonCommandRemote.PressButton(choice);
break;
case 2:
Console.WriteLine($"Chosen action: Off");
lastCommand = choice;
nonCommandRemote.PressButton(choice);
break;
case 3:
Console.WriteLine($"Chosen action: Undo");
nonCommandRemote.PressUndo(lastCommand);
break;
default:
Console.WriteLine("Chosen action: Unknown");
nonCommandRemote.PressButton(0);
break;
}
}
}
The output is identical to the Command Pattern implementation:
Performance Comparison
You might wonder why we should use the Command Pattern when the simplified approach achieves the same result with less code. A benchmark reveals that both approaches have similar performance, with the Command Pattern being slightly faster despite its larger codebase.
Why Use the Command Pattern?
While the simplified approach works for simple scenarios, the Command Pattern shines in more complex applications. Here’s why:
- Flexibility: You can easily add new commands without modifying existing code.
- Extensibility: Commands can be queued, logged, or executed in sequence.
- Decoupling: The invoker is decoupled from the receiver, promoting lower coupling and higher cohesion.
- Undo/Redo Support: The pattern naturally supports undoable operations.
Conclusion
The Command Pattern is a powerful tool for building flexible and maintainable systems. While it may seem like overkill for simple use cases, its benefits become evident as your application grows in complexity. By encapsulating actions as objects, you gain the ability to manage, extend, and reuse commands effortlessly.
You can find the source code by clicking the link.
I hope this article has been helpful! If you have any questions or feedback, feel free to leave a comment. Happy coding!
Top comments (1)
Thanks for sharing! I’ve implemented something similar using Blazor and the Command Pattern.
You can check out the code here:
github.com/stevsharp/BlazorAndComm...
I hope you find it interesting!