Skip to Content
Course content

110: The Command Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably encountered situations where your code feels too "tight." Maybe you have a UI button that directly calls a method on a database service, or a controller that knows way too much about the internal workings of a business logic class. When you do that, you're tying the invoker (the thing that triggers the action) directly to the receiver (the thing that actually does the work).

The Command Pattern is how we break that bond. It turns a request into a stand-alone object. This sounds like overkill until you realize it lets you queue operations, log them, or—my favorite—implement a robust undo system without making your main logic a nightmare of if-else statements.

Setting up our Smart Home gadgets

Let's build a smart home remote. To start, we need some actual devices to control. I'll keep these simple: a Light and a Thermostat. These are our "Receivers." They don't know anything about commands or remotes; they just know how to do their jobs.

public class Light
{
    public void TurnOn() => Console.WriteLine("The light is bright!");
    public void TurnOff() => Console.WriteLine("The light is off.");
}

public class Thermostat
{
    public void SetTemperature(int temp) => Console.WriteLine($"Temp set to {temp}°C");
}

The "quick and dirty" way

Now, I could just build a RemoteControl class and give it a reference to the light and the thermostat. I'll show you how I'd probably do it if I were rushing to meet a deadline on a Friday afternoon:

public class RemoteControl
{
    private Light _light;
    private Thermostat _thermostat;

    public RemoteControl(Light light, Thermostat thermostat)
    {
        _light = light;
        _thermostat = thermostat;
    }

    public void PressLightButton() => _light.TurnOn();
    public void PressTempButton() => _thermostat.SetTemperature(22);
}

Wait. I can already see the problem here. Every time I buy a new smart device—like a smart blind or a coffee maker—I have to go back into the RemoteControl class and add new properties and new methods. This is a violation of the Open/Closed Principle. The remote should be open for extension but closed for modification. We're treating the remote like a hard-wired switchboard instead of a universal controller.

Abstracting the action with ICommand

To fix this, I need to stop the remote from knowing what it's controlling. Instead, it should just know how to execute a "command." I'll create a simple interface that represents any action that can be triggered.

public interface ICommand
{
    void Execute();
    void Undo();
}

Now, I'll wrap those device-specific calls into their own classes. Each class acts as a bridge between the remote and the device. Notice how the LightOnCommand takes the Light object in its constructor; it knows who the receiver is, but the remote won't have to.

public class LightOnCommand : ICommand
{
    private readonly Light _light;
    public LightOnCommand(Light light) => _light = light;

    public void Execute() => _light.TurnOn();
    public void Undo() => _light.TurnOff();
}

public class TempSetCommand : ICommand
{
    private readonly Thermostat _thermostat;
    private readonly int _temp;
    private int _previousTemp = 20; // Simplified for this example

    public TempSetCommand(Thermostat thermostat, int temp)
    {
        _thermostat = thermostat;
        _temp = temp;
    }

    public void Execute()
    {
        _thermostat.SetTemperature(_temp);
    }

    public void Undo() => _thermostat.SetTemperature(_previousTemp);
}

Building a remote that doesn't care what it's controlling

Now I can rewrite the RemoteControl. Instead of hard-coding devices, I'll give it "slots" (like buttons) that hold an ICommand. The remote doesn't know if it's turning on a light, launching a missile, or ordering a pizza—it just calls Execute().

public class RemoteControl
{
    private ICommand _currentSlot;
    private ICommand _lastCommand;

    public void SetCommand(ICommand command)
    {
        _currentSlot = command;
    }

    public void PressButton()
    {
        _currentSlot.Execute();
        _lastCommand = _currentSlot;
    }

    public void PressUndo()
    {
        _lastCommand?.Undo();
    }
}

I love this approach because the RemoteControl is now completely generic. If you want to add a GarageDoorCommand tomorrow, you don't touch a single line of code in the RemoteControl class. You just create the new command class and plug it in at runtime.

Putting it all together

Here is how this looks in action. We instantiate the receivers, wrap them in commands, and hand those commands to the invoker.

var livingRoomLight = new Light();
var nestThermostat = new Thermostat();

var lightOn = new LightOnCommand(livingRoomLight);
var setWarm = new TempSetCommand(nestThermostat, 24);

var remote = new RemoteControl();

remote.SetCommand(lightOn);
remote.PressButton(); // Output: The light is bright!

remote.SetCommand(setWarm);
remote.PressButton(); // Output: Temp set to 24°C

remote.PressUndo();    // Output: Temp set to 20°C



📋 Practical Task

Building a Cinema Mode Macro Command

In the Command Pattern, a "Macro Command" is a command that contains a list of other commands and executes them in sequence. This is perfect for a "Cinema Mode" button that does multiple things at once.

Your Task: Create a class called MacroCommand that implements ICommand. It should take a list of ICommand objects in its constructor. When Execute() is called, it should loop through all the stored commands and call Execute() on each. Additionally, implement Undo() so that it reverses the commands in the opposite order they were executed.

Test your implementation by creating a CinemaMode macro that turns on a light, sets a temperature, and then (if you've added one) closes the blinds. When you call Undo() on the macro, ensure the devices return to their previous states in reverse order.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.