Skip to Content
Course content

375: The Command Pattern

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

The Command Pattern is one of those things that feels like "over-engineering" until the moment your boss asks you to implement a multi-level Undo system or a macro recording feature. At its core, the pattern is about turning a request into a standalone object. Instead of calling a method on an object directly, you wrap that call inside a "Command" object. This decouples the object that triggers the action from the object that actually knows how to perform it.

Setting up our smart home devices

To make this concrete, let's build a simple smart home controller. We have a few different devices—a light and a fan—that all do different things. In pattern terminology, these are our "Receivers." They hold the actual business logic.

class Light:
    def turn_on(self):
        print("The light is shining bright!")
    
    def turn_off(self):
        print("The light is now off.")

class Fan:
    def start(self):
        print("The fan is spinning.")
    
    def stop(self):
        print("The fan has stopped.")

Creating a unified way to trigger actions

Now, if we just call light.turn_on() from our main app, we're tightly coupled. If we want to add a "Macro" that turns on the light and the fan simultaneously, we'd have to write custom logic for every possible combination. Instead, I'll create a Command interface. In Python, we don't need a formal interface, but using an abstract base class makes our intentions clear.

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass

Wrapping the device logic in commands

Now I'll create concrete commands for our devices. Notice how the command object doesn't do the work itself; it just tells the receiver what to do. It's essentially a "pointer" to a specific action.

class LightOnCommand(Command):
    def __init__(self, light):
        self.light = light

    def execute(self):
        self.light.turn_on()

    def undo(self):
        self.light.turn_off()

class FanOnCommand(Command):
    def __init__(self, fan):
        self.fan = fan

    def execute(self):
        self.fan.start()

    def undo(self):
        self.fan.stop()

Where I messed up the Invoker

Next, I need an "Invoker"—the thing that actually triggers the command (like a remote control). When I first wrote this, I tried to be "helpful" by making the remote check what kind of command it was holding so it could call a specific method. It looked something like this:

# DON'T DO THIS
class RemoteControl:
    def submit(self, command):
        if isinstance(command, LightOnCommand):
            command.execute()
        elif isinstance(command, FanOnCommand):
            command.execute()

I realized immediately that I'd defeated the entire purpose of the pattern. If I add ten more devices, I have to keep updating this if-elif chain. The whole point of the Command Pattern is that the Invoker shouldn't care what the command is, only that it has an execute() method. I stripped that logic out and simplified it to a single line.

class RemoteControl:
    def __init__(self):
        self.history = []

    def press_button(self, command):
        command.execute()
        self.history.append(command)

    def press_undo(self):
        if self.history:
            command = self.history.pop()
            command.undo()
        else:
            print("Nothing to undo!")

Putting it all together

Now we can treat actions as data. I can pass them around, store them in a list, or trigger them based on a timer. The remote doesn't know it's talking to a light or a fan; it just knows it's executing a command.

# Setup
living_room_light = Light()
bedroom_fan = Fan()

light_on = LightOnCommand(living_room_light)
fan_on = FanOnCommand(bedroom_fan)

remote = RemoteControl()

# Execution
remote.press_button(light_on) # The light is shining bright!
remote.press_button(fan_on)   # The fan is spinning.

# Undo
remote.press_undo()           # The fan has stopped.
remote.press_undo()           # The light is now off.



📋 Practical Task

Exercise: Implementing a Macro Command for a "Movie Mode"

In the smart home example above, we can only execute one command at a time. Your task is to implement a MacroCommand. This is a special kind of command that holds a list of other commands and executes them all in sequence when its execute() method is called, and undoes them all (in reverse order) when its undo() method is called.

Requirements:

  • Create a class MacroCommand that inherits from Command.
  • It should take a list of commands in its __init__ method.
  • execute()` should loop through the commands and call their respective execute() methods.
  • undo()` should loop through the commands in reverse order and call their undo() methods.
  • Test your MacroCommand by creating a "Movie Mode" that turns on a light and starts a fan simultaneously using the RemoteControl.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.