Understanding the Command Pattern
The Command design pattern encapsulates a request as an object, thereby allowing clients to parameterize with different requests, queue requests, or log requests, and support undoable operations. Its fundamental purpose is to decouple the object that invokes an operation (the invoker) from the object that actually performs the operation (the receiver).
Key characteristics of the Command pattern:
- Request Encapsulation: A request is wrapped into an object, making it a first-class entity.
- Decoupling: The invoker doesn't need to know the specifics of the operation or the receiver that carries it out. It simply invokes a method on the command object.
- Extended Functionality: It facilitates advanced features like undo/redo mechanisms, transaction logging, and queuing requests for later execution.
Common applications include GUI button implementations, macro recording, transaction management, and task scheduling systems.
Core Components of the Command Pattern
The Command pattern typically involves four main participants:
- Command (
Operation): An interface that declares a method for executing an operation (e.g.,executeAction()). - ConcreteCommand (e.g.,
TurnLightOnCmd): Implements the Command interface. It binds together a Receiver object with an action. TheexecuteAction()method calls the appropriate action on the Receiver. - Receiver (e.g.,
RoomLight,HomeGarageDoor): The object that knows how to perform the actual work. It contains the business logic for the operations. - Invoker (e.g.,
UniversalRemoteController): Holds a Command object and, at the appropriate time, asks the command to carry out the request. It doesn't know anything about the concrete command or receiver. - Client (e.g.,
RemoteControlSimulator): Creates a ConcreteCommand object and sets its receiver.
Practical Example: A Smart Home Remote Control System
Consider a simple home automation system where a universal remote controls various devices like lights and a garage door. We can use the Command pattern to manage these diverse operations.
1. Defining Device Receivers
First, let's define our receiver classes, which are the devices that will perform the actual actions.
RoomLight.java
package com.app.commands.devices;
public class RoomLight {
private String location;
public RoomLight(String location) {
this.location = location;
}
public void activate() {
System.out.println(location + " light is now ON.");
}
public void deactivate() {
System.out.println(location + " light is now OFF.");
}
}
HomeGarageDoor.java
package com.app.commands.devices;
public class HomeGarageDoor {
public HomeGarageDoor() {
}
public void raise() {
System.out.println("Garage Door is OPEN.");
}
public void lower() {
System.out.println("Garage Door is CLOSED.");
}
public void halt() {
System.out.println("Garage Door movement HALTED.");
}
public void turnLightOn() {
System.out.println("Garage light is ON.");
}
public void turnLightOff() {
System.out.println("Garage light is OFF.");
}
}
2. Defining the Command Interface
This interface declares the method that the invoker will call to execute an operation.
Operation.java
package com.app.commands.devices;
public interface Operation {
void executeAction();
}
3. Implementing Concrete Commands
These classes implement the Operation interface and encapsulate a specific action on a particular receiver.
TurnLightOnCmd.java
package com.app.commands.devices;
public class TurnLightOnCmd implements Operation {
private RoomLight targetLight;
public TurnLightOnCmd(RoomLight targetLight) {
this.targetLight = targetLight;
}
@Override
public void executeAction() {
targetLight.activate();
}
}
TurnLightOffCmd.java
package com.app.commands.devices;
public class TurnLightOffCmd implements Operation {
private RoomLight targetLight;
public TurnLightOffCmd(RoomLight targetLight) {
this.targetLight = targetLight;
}
@Override
public void executeAction() {
targetLight.deactivate();
}
}
OpenGarageDoorCmd.java
package com.app.commands.devices;
public class OpenGarageDoorCmd implements Operation {
private HomeGarageDoor garageDevice;
public OpenGarageDoorCmd(HomeGarageDoor garageDevice) {
this.garageDevice = garageDevice;
}
@Override
public void executeAction() {
garageDevice.raise();
}
}
CloseGarageDoorCmd.java
package com.app.commands.devices;
public class CloseGarageDoorCmd implements Operation {
private HomeGarageDoor garageDevice;
public CloseGarageDoorCmd(HomeGarageDoor garageDevice) {
this.garageDevice = garageDevice;
}
@Override
public void executeAction() {
garageDevice.lower();
}
}
4. Building the Invoker (UniversalRemoteController)
The invoker holds a command and triggers its execution without knowing the command's specifics or the receiver.
UniversalRemoteController.java
package com.app.commands.devices;
public class UniversalRemoteController {
private Operation activeCommand;
public UniversalRemoteController() {
// Default constructor
}
public void assignCommand(Operation commandToExecute) {
this.activeCommand = commandToExecute;
}
public void triggerOperation() {
if (activeCommand != null) {
activeCommand.executeAction();
} else {
System.out.println("No command assigned to the remote slot.");
}
}
}
5. Simulating Remote Control Usage (Client)
The client creates the receivers and concrete commands, then configures the invoker with the desired commands.
RemoteControlSimulator.java
package com.app.commands.devices;
public class RemoteControlSimulator {
public static void main(String[] args) {
UniversalRemoteController remote = new UniversalRemoteController();
// Instantiate receivers
RoomLight livingRoomLight = new RoomLight("Living Room");
HomeGarageDoor mainGarageDoor = new HomeGarageDoor();
// Create concrete commands
TurnLightOnCmd turnLivingRoomLightOn = new TurnLightOnCmd(livingRoomLight);
TurnLightOffCmd turnLivingRoomLightOff = new TurnLightOffCmd(livingRoomLight);
OpenGarageDoorCmd openMainGarageDoor = new OpenGarageDoorCmd(mainGarageDoor);
CloseGarageDoorCmd closeMainGarageDoor = new CloseGarageDoorCmd(mainGarageDoor);
System.out.println("--- Testing Living Room Light ---");
remote.assignCommand(turnLivingRoomLightOn);
remote.triggerOperation(); // Output: Living Room light is now ON.
remote.assignCommand(turnLivingRoomLightOff);
remote.triggerOperation(); // Output: Living Room light is now OFF.
System.out.println("\n--- Testing Garage Door ---");
remote.assignCommand(openMainGarageDoor);
remote.triggerOperation(); // Output: Garage Door is OPEN.
remote.assignCommand(closeMainGarageDoor);
remote.triggerOperation(); // Output: Garage Door is CLOSED.
// You can easily reconfigure the remote for different commands,
// even using an anonymous inner class for a simple, ad-hoc command.
System.out.println("\n--- Reconfiguring remote for garage light ---");
Operation turnGarageLightOn = new Operation() {
@Override
public void executeAction() {
mainGarageDoor.turnLightOn();
}
};
remote.assignCommand(turnGarageLightOn);
remote.triggerOperation(); // Output: Garage light is ON.
}
}