Skip to content

๐Ÿ•น๏ธ Command Pattern: Programmable Smart Home Hub

๐Ÿ“ Overview

The Command Pattern encapsulates a request as a standalone object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. It decouples the object that invokes the operation from the one that knows how to perform it.

Core Concepts

  • Encapsulation of Request: Turning a method call into an object.
  • Decoupling: The Invoker (remote) knows nothing about the Receiver (light bulb).
  • Undo/Redo: Commands can store state to reverse their effects.
  • Macro Commands: Composing multiple commands into a single "macro" for batch execution.

๐Ÿญ The Engineering Story & Problem

๐Ÿ˜ก The Villain (The Problem)

Imagine a "Hardwired Remote Control" where every button is soldered directly to a specific device's circuit. Button 1 is hardwired to the Living Room Light. Button 2 to the Stereo.
If you want to change Button 1 to turn on the Kitchen Light instead, you have to physically rewire the remote (change the Remote class code). The remote acts like a God Object that needs to know the specific API of every device (light.turnOn(), stereo.setVolume(), door.lock()). This tight coupling makes the system rigid and hard to extend.

๐Ÿฆธ The Hero (The Solution)

The Command Pattern introduces a "Universal Connector." We create a standard Command interface with an execute() method. We wrap every specific device action (like "Turn on Light") into its own little class (e.g., LightOnCommand).
The remote just holds a list of these Command objects. When you press a button, it just says command.execute(). It doesn't care if it's turning on a light or launching a missile. You can swap commands in and out dynamically without touching the remote's code.

๐Ÿ“œ Requirements & Constraints

  1. (Functional): The Remote Control must have programmable slots that can be assigned any command.
  2. (Functional): Support an "Undo" button that reverses the last action.
  3. (Technical): The Remote must be decoupled from specific device implementations (Light, Stereo, TV).

๐Ÿ—๏ธ Structure & Blueprint

Class Diagram

classDiagram
    direction TB
    class Command {
        <<interface>>
        +execute()
        +undo()
    }
    class LightOnCommand {
        -light: Light
        +execute()
        +undo()
    }
    class StereoOnWithCDCommand {
        -stereo: Stereo
        +execute()
        +undo()
    }
    class RemoteControl {
        -onCommands: Command[]
        -offCommands: Command[]
        -undoCommand: Command
        +setCommand(slot, onCommand, offCommand)
        +onButtonWasPushed(slot)
        +offButtonWasPushed(slot)
        +undoButtonWasPushed()
    }
    class Light {
        +on()
        +off()
    }

    Command <|.. LightOnCommand
    Command <|.. StereoOnWithCDCommand
    RemoteControl o-- Command
    LightOnCommand --> Light : receiver
    StereoOnWithCDCommand --> Stereo : receiver

Runtime Context (Sequence)

sequenceDiagram
    participant Client
    participant Remote
    participant LightOnCmd
    participant Light

    Client->>Remote: setCommand(0, LightOnCmd, LightOffCmd)
    Client->>Remote: onButtonWasPushed(0)
    Remote->>LightOnCmd: execute()
    LightOnCmd->>Light: on()

    Client->>Remote: undoButtonWasPushed()
    Remote->>LightOnCmd: undo()
    LightOnCmd->>Light: off()

๐Ÿ’ป Implementation & Code

๐Ÿง  SOLID Principles Applied

  • Single Responsibility: The Remote only knows how to trigger commands; Commands only know how to map trigger to action; Devices only know how to perform actions.
  • Open/Closed: You can add new commands (e.g., GarageDoorOpenCommand) without changing the Remote code.

๐Ÿ The Code

The Villain's Code (Without Pattern)
class RemoteControl:
    def on_button_pressed(self, slot):
        # ๐Ÿ˜ก Tight coupling and rigid logic
        if slot == 0:
            self.living_room_light.on()
        elif slot == 1:
            self.kitchen_light.on()
        elif slot == 2:
            self.stereo.on()
            self.stereo.set_cd()
            self.stereo.set_volume(11)
        # If we want to change slot 0, we must edit this class!
The Hero's Code (With Pattern)
from abc import ABC, abstractmethod


class Light:
    def __init__(self) -> None:
        self.intensity: int = 0
    def on(self) -> None:
        self.intensity = 100
        print("Light is ON")

    def off(self) -> None:
        self.intensity = 0
        print("Light is OFF")

    def set_intensity(self, intensity: int) -> None:
        self.intensity = intensity
        print(f"Light is set to the intensity: {intensity}")


class Stereo:
    def __init__(self) -> None:
        self.volume: int = 0
        self.hasCD: bool = False

    def increaseVolume(self) -> None:
        volume = self.volume + 1
        self.volume = volume
        print(f"Volume has been increased to {volume}")

    def setVolume(self, volume: int) -> None:
        self.volume = volume
        print(f"Volume has been set to {volume}")

    def getVolume(self) -> int:
        return self.volume

    def insertCD(self) -> None:
        self.hasCD = True
        print("CD has been inserted")

    def ejectCD(self) -> None:
        self.hasCD = False
        print("CD has been ejected")

    def playCD(self) -> None:
        if self.hasCD:
            print("Playing the CD!!")
        else:
            print("Please insert CD to play")

    def mute(self) -> None:
        self.volume = 0
        print("TV has been muted")


class Garage:
    def __init__(self) -> None:
        self.state: bool = False

    def open(self) -> None:
        if self.state:
            print("The Garage is already Opened")
        else:
            self.state = True
            print("Opening the Garage")

    def close(self) -> None:
        if self.state:
            self.state = False
            print("Closing the Garage")
        else:
            print("The Garage is already Closed")

    def toggle(self) -> None:
        if self.state:
            self.state = False
            print("Closing the Garage")
        else:
            self.state = True
            print("Opening the Garage")


class Thermostat:
    def __init__(self) -> None:
        self.temp: int = 74

    def set_temp(self, temp: int) -> None:
        self.temp = temp
        print(f"Thermostat has been set to temperature: {temp}ยฐF")


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

    def undo(self) -> None:
        pass


class LightOnCommand(Command):
    def __init__(self, light: Light) -> None:
        self.light: Light = light
        self.prev_state: Light = light

    def execute(self) -> None:
        self.light.on()

    def undo(self) -> None:
        light: Light = self.light
        self.light = self.prev_state
        self.prev_state = light


class LightDimCommand(Command):
    def __init__(self, light: Light) -> None:
        self.light: Light = light
        self.prev_state: Light = light

    def execute(self) -> None:
        self.light.set_intensity(intensity=30)

    def undo(self) -> None:
        light: Light = self.light
        self.light = self.prev_state
        self.prev_state = light


class LightOffCommand(Command):
    def __init__(self, light: Light) -> None:
        self.light: Light = light
        self.prev_state: Light = light

    def execute(self) -> None:
        self.light.off()

    def undo(self) -> None:
        light: Light = self.light
        self.light = self.prev_state
        self.prev_state = light


class StereoIncreaseVolumeCommand(Command):
    def __init__(self, stereo: Stereo) -> None:
        self.stereo: Stereo = stereo
        self.prev_state: Stereo = stereo

    def execute(self) -> None:
        self.stereo.increaseVolume()

    def undo(self) -> None:
        stereo: Stereo = self.stereo
        self.stereo = self.prev_state
        self.prev_state = stereo


class StereoMuteCommand(Command):
    def __init__(self, stereo: Stereo) -> None:
        self.stereo: Stereo = stereo
        self.prev_state: Stereo = stereo

    def execute(self) -> None:
        self.stereo.setVolume(volume=0)

    def undo(self) -> None:
        stereo: Stereo = self.stereo
        self.stereo = self.prev_state
        self.prev_state = stereo



class GarageOpenCommand(Command):
    def __init__(self, garage: Garage) -> None:
        self.garage: Garage = garage
        self.prev_state: Garage = garage

    def execute(self) -> None:
        self.garage.open()

    def undo(self) -> None:
        self.garage.toggle()


class PartyCommand(Command):
    def __init__(self, thermostat: Thermostat, light: Light, stereo: Stereo) -> None:
        self.thermostat: Thermostat = thermostat
        self.prev_thermo: Thermostat = thermostat
        self.light: Light = light
        self.prev_light: Light = light
        self.stereo: Stereo = stereo
        self.prev_stereo: Stereo = stereo

    def execute(self) -> None:
        self.thermostat.set_temp(temp=72)
        self.light.set_intensity(intensity=30)
        self.stereo.playCD()

    def undo(self) -> None:
        thermostat: Thermostat = self.thermostat
        self.thermostat = self.prev_thermo
        self.prev_thermo = thermostat

        light: Light = self.light
        self.light = self.prev_light
        self.prev_light = light

        stereo: Stereo = self.stereo
        self.stereo = self.prev_stereo
        self.prev_stereo = stereo


class Remote:
    def __init__(self, button_1: Command, button_2: Command, button_3: Command, button_4: Command, party: Command) -> None:
        self.button_1: Command = button_1
        self.button_2: Command = button_2
        self.button_3: Command = button_3
        self.button_4: Command = button_3
        self.party: Command = party
        self.undo: Command | None = None

    def firstButton(self) -> None:
        self.undo = self.button_1
        self.button_1.execute()

    def secondButton(self) -> None:
        self.undo = self.button_2
        self.button_2.execute()

    def thirdButton(self) -> None:
        self.undo = self.button_3
        self.button_3.execute()

    def fourthButton(self) -> None:
        self.undo = self.button_4
        self.button_4.execute()

    def partyButton(self) -> None:
        self.undo = self.party
        self.party.execute()

    def undoButton(self) -> None:
        if self.undo:
            self.undo.undo()

my_thermo = Thermostat()
my_light = Light()
my_stereo = Stereo()
my_garage = Garage()

light_on_command = LightOnCommand(light=my_light)
light_dim_command = LightDimCommand(light=my_light)
open_garage_command = GarageOpenCommand(garage=my_garage)
mute_command = StereoMuteCommand(stereo=my_stereo)
party_command = PartyCommand(thermostat=my_thermo, light=my_light, stereo=my_stereo)

my_remote = Remote(button_1=light_on_command, button_2=light_dim_command, button_3=open_garage_command, button_4=mute_command, party=party_command)

my_remote.firstButton()
my_remote.fourthButton()
my_remote.undoButton()

โš–๏ธ Trade-offs & Testing

Pros (Why it works) Cons (The Twist / Pitfalls)
Decoupling: Invoker and Receiver are independent. Class Explosion: Every action requires a new concrete Command class.
Extensibility: Easy to add new commands or Macro commands. Complexity: Can feel like overkill for simple callback logic.
Undo/Redo: State can be saved in the command to reverse it. Memory: Keeping a history of commands for unlimited undo consumes RAM.

๐Ÿงช Testing Strategy

  1. Unit Test Commands: verify that LightOnCommand.execute() calls light.on().
  2. Test Remote (Invoker): Verify that the remote calls execute() on the injected mock command.
  3. Test Undo: Execute a command, then call undo, and verify the state is rolled back.

๐ŸŽค Interview Toolkit

  • Interview Signal: mastery of encapsulation, callbacks vs objects, and transactional behavior (undo).
  • When to Use:
    • "Implement a menu system where actions are configurable..."
    • "Build a task queue or job scheduler..."
    • "Support Undo/Redo functionality..."
  • Scalability Probe: "How to handle thousands of commands?" (Answer: Use a command queue/worker pool pattern. Serializing commands to DB allows persistent queues.)
  • Design Alternatives:
    • Strategy: Similar (encapsulating behavior), but Strategy is about how to do something, Command is about what to do.
    • Memento: Often used with Command to save state for undo.