๐น๏ธ 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
- (Functional): The Remote Control must have programmable slots that can be assigned any command.
- (Functional): Support an "Undo" button that reverses the last action.
- (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
Remoteonly knows how to trigger commands;Commandsonly know how to map trigger to action;Devicesonly know how to perform actions. - Open/Closed: You can add new commands (e.g.,
GarageDoorOpenCommand) without changing theRemotecode.
๐ 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
- Unit Test Commands: verify that
LightOnCommand.execute()callslight.on(). - Test Remote (Invoker): Verify that the remote calls
execute()on the injected mock command. - 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.
๐ Related Patterns
- Memento โ Used to store state for Command's undo.
- Chain of Responsibility โ A command can be passed along a chain.
- Composite โ MacroCommands are Composites of Commands.