The Command Design Pattern is a behavioral pattern that turns a request into a standalone object, allowing you to parameterize actions, queue them, log them, or support undoable operations — all while decoupling the sender from the receiver.
It’s particularly useful in situations where:
- You want to encapsulate operations as objects.
- You need to queue, delay, or log requests.
- You want to support undo/redo functionality.
- You want to decouple the object that invokes an operation from the one that knows how to perform it.
Let’s walk through a real-world example to see how we can apply the Command Pattern to decouple invokers from executors, and build a more flexible, extensible, and testable command execution framework.
1. The Problem: The Tightly Coupled Smart Home Controller
Imagine you're building a "Smart Home Hub" application.
This hub needs to control various devices:
- 💡 Smart lights
- 🌡️ Thermostats
- 🔒 Security systems
- 🔊 Smart speakers
- 🚪 Garage doors
The hub should be able to send commands like:
light.on()
, light.off()
thermostat.setTemperature(22)
speaker.playMusic()
Naive Implementation: One Controller to Rule Them All
You might start with a simple class like this:
Light
Thermostat
Controller tightly coupled to devices
Example Usage
Why This Design Fails as the System Grows
This simple controller works for now, but quickly falls apart as complexity increases.
1. Tight Coupling
- The
SmartHomeControllerV1
class is directly tied to every device and their specific method names. - You can’t reuse or generalize actions — every new device requires a new method in the controller.
2. Poor Scalability
Adding a new device (e.g., Sprinkler
, GarageDoor
, SmartSpeaker
) means:
- Adding new fields to the controller
- Writing more methods for each action
- Increasing the size and complexity of one giant class
Soon, your controller becomes a bloated monolith.
3. No Undo/Redo Support
There's no way to reverse a command. Want to implement an “undo last action” feature? You’d need to:
- Track device states manually
- Write custom undo logic for every method
- Add a large switch/if-else block somewhere to figure out what action to reverse
It’s fragile, repetitive, and error-prone.
4. No Scheduling or Queuing
Imagine a user sets a rule like:
"Turn on the lights at 7:00 PM"
You can’t queue up what to do, because actions are hardcoded into method calls — not represented as standalone objects.
5. No Generic Logging
- Want to log every action taken?
- Or store a history of operations to replay them later?
Not possible without duplicating logging code inside each method.
6. Difficult UI Mapping
- Suppose you're building a remote control with programmable buttons.
- Or a mobile app with customizable scenes like "Movie Night" or "Leave Home".
- Each UI button would need custom logic to know which device to call and how — tightly coupled, hard to scale, and impossible to reuse.
What We Really Need
We need to treat each command (e.g., “turn on light”, “set thermostat to 22°C”) as a standalone object — something that encapsulates:
- What to do
- Which device it affects
- How to execute it
- (Optionally) How to undo it
This way, the controller, remote, or scheduler doesn’t care how a command works — it just knows which command to execute.
This is exactly what the Command Design Pattern enables.
2. What is the Command Pattern
The Command Design Pattern is a behavioral pattern that turns a request into a standalone object, allowing you to:
- Parameterize actions
- Queue or log operations
- Support undo/redo
- Decouple the invoker of an operation from the receiver that performs it
Instead of calling methods directly on device classes, you encapsulate each request (like light.on()
or thermostat.setTemperature(22)
) as a Command object.
Class Diagram
1. Command (Interface)
- Defines a standard interface for executing operations — typically an
execute()
method. - May also declare an
undo()
method if undo functionality is required. - Serves as the abstraction that all concrete commands will implement.
2. ConcreteCommand
- Implements the
Command
interface. - Maintains a reference to the Receiver, and implements
execute()
by delegating to the receiver’s method(s). - May also capture the necessary state to support
undo()
.
3. Receiver
- The object that performs the actual work.
- It contains the business logic that will be triggered by a command.
- The receiver is not aware of the command or the invoker — it simply exposes the operations.
4. Invoker
- Responsible for initiating command execution.
- It holds a reference to a
Command
object and calls execute()
when an action (e.g., button press) occurs. - The invoker does not know the specifics of the command or the receiver — it interacts only with the Command interface.
5. Client
- Creates instances of ConcreteCommand, associating each with the appropriate Receiver.
- Configures the Invoker with the command(s) to execute.
- The client decides what actions to perform and sets everything up — but does not execute the commands itself.
How It Works
- Each Command implements a standard interface like
execute()
(and optionally undo()
) - The Invoker (e.g., remote control, scheduler, UI) simply calls
command.execute()
- The Receiver (e.g.,
Light
, Thermostat
) performs the actual operation when the command is executed
This separation allows for highly flexible, decoupled, and extensible designs.
3. Implementing Command Pattern
Let’s refactor our Smart Home Controller to use the Command Pattern with support for undoable actions. We'll encapsulate each action as a command, decouple the invoker from the logic, and allow undoing previous actions.
1. Define the Command
Interface
All commands must implement execute()
and undo()
.
2. Define the Receivers (Devices)
These are the actual smart home devices that perform the actions.
💡 Light
🌡️ Thermostat
3. Implement Concrete Command Classes
Each command encapsulates a specific action and knows how to undo it.
LightOnCommand
LightOffCommand
SetTemperatureCommand
4. Create the Invoker (SmartButton
) with Undo Support
5. Client Code – Using the Command System
Output
What We Achieved
- Encapsulated Commands: Each action is a reusable, undoable object
- Decoupled UI/Logic: Invoker doesn’t know how a command works
- Undo Support: Each command tracks and reverts its own effect
- Extensibility: Easily add
PlayMusicCommand
, OpenGarageCommand
, etc. - History Tracking: Command history enables undo/redo or logging