Command
Behavioral Design Patterns
Imagine you are developing a "smart home" application with a UI that includes buttons to control various devices like lights, stereos, and garage doors.
A naive approach would be to code the logic directly inside each button's event listener:
public class SmartHomeUI {
private Light livingRoomLight = new Light();
private Stereo mainStereo = new Stereo();
public void onLightOnButtonClick() {
livingRoomLight.on();
}
public void onStereoOnButtonClick() {
mainStereo.on();
mainStereo.setCd();
mainStereo.setVolume(11);
}
// ... and so on
}
This UI class is now tightly coupled to the Light
and Stereo
classes. What happens when you want to add a Television
? You have to modify the SmartHomeUI
class. What if you want to reuse these actions in a different context, like a voice-activated assistant? You'd have to duplicate the logic.
Furthermore, how would you implement a global "undo" button? It's nearly impossible because the UI has no record of the actions it has performed; it just called methods directly.
Encapsulating a Request as an Object
The Command is a behavioral design pattern that turns a request into a stand-alone object containing all information about the request. This object, the "command," serves as a decoupling layer between the object that invokes an operation and the object that knows how to perform it.
The invoker (e.g., a button) doesn't call a method on the receiver (e.g., a light) directly. Instead, it calls an execute()
method on the command object. The command object itself holds a reference to the receiver and knows which of the receiver's methods to call. This encapsulation allows you to parameterize objects with operations, queue requests, and, most famously, support undoable operations.
The Participants
- Command: The interface for all command objects. It typically declares a single method,
execute()
, and often anundo()
method. - Concrete Command: A specific implementation of the
Command
interface. It holds a reference to aReceiver
object and knows how to trigger the correct action on it. - Receiver: The object that performs the actual work (e.g., the
Light
class). It has the business logic. - Invoker: The object that triggers the command (e.g., a
RemoteControl
button). It holds aCommand
object and calls itsexecute()
method. - Client: The application that wires everything together: it creates the receivers, creates the concrete commands, and associates them with invokers.
Example: A Smart Remote Control
Let's build a simple remote control using the Command pattern.
// The Command interface
public interface ICommand {
void execute();
void undo();
}
// The Receiver class
public class Light {
public void on() { System.out.println("Light is ON"); }
public void off() { System.out.println("Light is OFF"); }
}
// A Concrete Command to turn the light on
public class LightOnCommand implements ICommand {
private final Light light;
public LightOnCommand(Light light) { this.light = light; }
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off(); // The undo operation for "on" is "off"
}
}
// A Concrete Command to turn the light off
public class LightOffCommand implements ICommand {
private final Light light;
public LightOffCommand(Light light) { this.light = light; }
@Override
public void execute() {
light.off();
}
@Override
public void undo() {
light.on();
}
}
// The Invoker class
public class RemoteControl {
private ICommand command;
public void setCommand(ICommand command) {
this.command = command;
}
public void onButtonWasPushed() {
command.execute();
}
public void onUndoButtonWasPushed() {
command.undo();
}
}
// The Client, which wires everything up
public class Client {
public static void main(String[] args) {
RemoteControl remote = new RemoteControl();
Light livingRoomLight = new Light();
// Create commands and associate them with the receiver
ICommand lightOn = new LightOnCommand(livingRoomLight);
ICommand lightOff = new LightOffCommand(livingRoomLight);
// Turn the light on
remote.setCommand(lightOn);
System.out.println("--- Pushing ON button ---");
remote.onButtonWasPushed(); // Output: Light is ON
// Press undo
System.out.println("--- Pushing UNDO button ---");
remote.onUndoButtonWasPushed(); // Output: Light is OFF
}
}
Interview Focus: Analysis and Trade-offs
Benefits:
- Decoupling: The key benefit. The
RemoteControl
(Invoker) is completely decoupled from theLight
(Receiver). The remote could be reprogrammed to control aStereo
or aGarageDoor
just by giving it a different command object, without changing its code. - Commands as First-Class Objects: Since requests are now objects, they can be manipulated like any other object: stored in a list, passed as method parameters, queued, logged, etc.
- Undo/Redo Functionality: The pattern provides a clean and logical structure for implementing undo and redo operations. Each command object knows how to reverse its own action.
- Composition: You can assemble a sequence of commands into a single
MacroCommand
to execute them as a single batch operation.
Drawbacks:
- Increased Complexity: The primary trade-off is an increase in the number of classes. For every simple action, you need to create a whole new
ConcreteCommand
class, which can lead to a lot of boilerplate code.
How Command Relates to Other Concepts
- Object-Oriented Callbacks: The Command pattern can be seen as an object-oriented replacement for callback functions. Instead of passing a function pointer to the invoker, you pass a command object.
- Queuing and Asynchronous Operations: Because commands are objects, they are perfect for queuing. You can add command objects to a queue, and a pool of worker threads can process them asynchronously. This is fundamental to job/task processing systems.
- Composite Pattern: A
MacroCommand
that executes a sequence of other commands is a direct application of the Composite pattern. The invoker can treat the macro (a composite) the same way it treats a single command (a leaf). - Transactional Behavior: A sequence of commands can be wrapped in a transaction. If one command in the sequence fails, you can iterate through the history of executed commands and call their
undo()
methods to roll back the entire transaction.