Introduction
The Command Design Pattern is a good example of encapsulation and allows us to encapsulate method invocations. Let us learn about the Command pattern.
Command Design Pattern - Definition
The Command Design Pattern encapsulates a request as an object, thereby letting you parameterize other objects with different requests, queue or log requests, and support undoable operations.
Home Automation use-case
Usually, I give my own example for design patterns. But this time, I’m going to use the same example as in the Head First Design Patterns book (but with some modifications) as I felt it was the most accurate (and easily understandable) one.
From a third-party vendor, we have classes or code which implements various home automation functionalities like controlling the living room light, ceiling fan, TV, garage door, etc. We have a remote with a pre-determined number of slots having on and off button to control the various household devices. For example: The on and off buttons for ceiling fan turn on and off the ceiling fan.
Home automation classes
Some of the household device classes we got from the third party are shown below.
First, we have the Light class. It has two methods to turn on and off the light.
public class Light {
/**
* Method to switch off the light.
*/
public void off() {
System.out.println("Switching off the light");
}
/**
* Method to switch on the light.
*/
public void on() {
System.out.println("Switching on the light");
}
}
The CeilingFan class has methods to set the fan to high, medium and low speeds. It also has a method to get the current speed of the fan.
public class CeilingFan {
private static int HIGH_SPEED = 3;
private static int MEDIUM_SPEED = 2;
private static int LOW_SPEED = 1;
private int speed = 0;
/**
* Set the speed of the ceiling fan to high.
*/
public void setSpeedToHigh() {
System.out.println("Setting fan speed to high");
this.speed = HIGH_SPEED;
}
/**
* Set the speed of the ceiling fan to medium.
*/
public void setSpeedToMedium() {
System.out.println("Setting fan speed to medium");
this.speed = MEDIUM_SPEED;
}
/**
* Set the speed of the ceiling fan to low.
*/
public void setSpeedToLow() {
System.out.println("Setting fan speed to low");
this.speed = LOW_SPEED;
}
/**
* Turn off the fan
*/
public void turnOff() {
System.out.println("Turning off the fan");
this.speed = 0;
}
/**
* Returns the current fan speed
*
* @return the current fan speed.
*/
public int getSpeed() {
return this.speed;
}
}
The Stereo class has methods to turn the stereo on and off and can also set the volume using the setVolume method.
public class Stereo {
private int volume;
/**
* Turn on the stereo.
*/
public void on() {
System.out.println("Turning on the stereo");
}
/**
* Turn off the stereo.
*/
public void off() {
System.out.println("Turning off the stereo");
}
/**
* Set the volume of the stereo.
* Note: Volume must be between 0 to 10.
*/
public void setVolume(int volume) {
if (volume < 0 || volume > 10) {
throw new IllegalArgumentException("Invalid volume. Volume must be between 0 to 10");
}
System.out.println("Setting the volume of stereo to " + volume);
this.volume = volume;
}
}
Building the home automation remote controller
Let us now see how we can implement the remote controller to control these home automation devices without using the command design pattern first.
The device will have buttons to control these appliances viz.,
- Turn on and off the light (2 slots or buttons).
- Control the fan (set fan to high or medium or low or turn it off - 4 buttons).
- Turn on and off the stereo (2 buttons) - let us assume we always set the volume to a default value when we turn the stereo on.
The RemoteController class is shown below. We have 8 slots in total. Let us assume the buttons are 0-based indexed. The RemoteController class has references to each of the household device classes. Depending on which button we press, it does the appropriate action.
public class RemoteControllerV1 {
private final Light light;
private final Stereo stereo;
private final CeilingFan ceilingFan;
public RemoteControllerV1(Light light, Stereo stereo, CeilingFan ceilingFan) {
this.light = light;
this.stereo = stereo;
this.ceilingFan = ceilingFan;
}
public void buttonPressed(int slotNum) {
switch (slotNum) {
// slot 0 and 1 are for light
case 0:
light.on();
break;
case 1:
light.off();
break;
// slot 2 and 3 are for stereo
case 2:
stereo.on();
stereo.setVolume(8);
break;
case 3:
stereo.off();
break;
// slot 4 to 7 are for fan
case 4:
ceilingFan.setSpeedToLow();
break;
case 5:
ceilingFan.setSpeedToMedium();
break;
case 6:
ceilingFan.setSpeedToHigh();
break;
case 7:
ceilingFan.turnOff();
break;
default:
throw new IllegalArgumentException("Invalid button slot number: " + slotNum);
}
}
}
Let us see this in action. We first create instances of the household device classes and pass them to the remote controller class. Then we press all the buttons and test them.
public class RemoteControllerV1Demo {
public static void main(String[] args) {
Light light = new Light();
Stereo stereo = new Stereo();
CeilingFan ceilingFan = new CeilingFan();
RemoteControllerV1 remoteControllerV1 = new RemoteControllerV1(light, stereo, ceilingFan);
// Controlling the light
remoteControllerV1.buttonPressed(0);
remoteControllerV1.buttonPressed(1);
System.out.println();
// Controlling the stereo
remoteControllerV1.buttonPressed(2);
remoteControllerV1.buttonPressed(3);
System.out.println();
// Controlling the fan
remoteControllerV1.buttonPressed(4);
remoteControllerV1.buttonPressed(5);
remoteControllerV1.buttonPressed(6);
remoteControllerV1.buttonPressed(7);
}
}
- Pressing button 0 turns on the light and pressing button 1 turns it off.
- Pressing button 4 turns on the fan and sets its speed to low and so on.
Running the above code, we get,
Switching on the light
Switching off the light
Turning on the stereo
Setting the volume of stereo to 8
Turning off the stereo
Setting speed to low
Setting speed to medium
Setting speed to high
Turning off the fan
Problems with the remote controller implementation
There are several problems with our naive implementation of remote controller.
First, there is a tight coupling between the remote controller class and the household device classes. When we change one of the device classes (say introducing a new fan speed), then the remote controller class will be affected.
Second, because of the above mentioned coupling, when new household devices are added (say to control the garage door), the remote controller class has to be changed.
Finally, the code in the remote controller knows a lot of internal details of the device classes. For example, it knows that to turn on the stereo, it also has to set the volume of it.
Using Command Design Pattern
When we use the Command Pattern, we can decouple the caller (remote controller) from the receiver (device classes) which will do some function. Hence, the object (caller) which wants to get something done doesn’t know or care about:
- Who performs the action and
- How the action is performed
In short, the command pattern allows us to decouple the requester of an action from the object that actually performs the action.
We achieve this by using “command objects”. A command object encapsulates a request to do something (turn on the light) on a specific object (the light object). We create multiple command objects each for doing some action. We then store or assign the command object against each button slot.
When we press a button, the button asks the command object to do the work (e.g., turn on the light) and the command object takes care of the rest. This way, the remote controller (caller) doesn’t have any idea on who does the work or how it is done. The caller (remote) talks to the command object and the command object talks to the device classes. Each device class which actually performs the work is known as the receiver here.
Thus by using the command pattern, the remote code is simple and decoupled. The command objects encapsulate how to do a home automation task along with the object that needs to do it.
Command Pattern - Mechanics
Before we update the remote controller by introducing the command pattern, let us first learn about it and the actors involved in it.
Command Pattern - Participants
Receiver
First, we have the receiver objects. These are the objects which perform some action or work. These are the household device classes in our example (Light, CeilingFan etc.,).
Command object
The command object provides an interface which has one method. Let’s call it execute(). The goal of the command object is to encapsulate the work being done. It has a reference to the receiver object, which does the work, and the command object invokes one or more actions on the receiver object.
In our example, we can encapsulate turning on the stereo within the command object’s execute() method (turning on the stereo followed by setting the volume).
Invoker
The invoker has the command object (one or more of them) and it just calls the execute() method on the command object to do the work. It doesn’t know anything about the receiver that does the work or about the actual action that takes place.
Command Design Pattern in action
Let us define the Command interface as,
public interface Command {
/**
* Execute the command
*/
void execute();
}
It is a very simple interface. It has a method to execute the command. Simple!!
Let us create a concrete command object which implements the Command interface.
public class LightOnCommand implements Command {
private final Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
This is a command object which does the work of turning on a light. We pass a specific Light instance which it will turn on. The Light is the receiver object which actually does the work, i.e., the light is the receiver of the request.
The execute method of the command object calls the on() method on the light (receiver).
Finally, let us create a very simple remote having only one command (one button slot). Later, we will update our original remote to support all operations.
The remote has a reference to a command object. When we press a button (the button here), it delegates the responsibility to do the work to the command object. And as seen before, the command object will call the receiver object to do the work.
public class SimpleRemote {
private final Command command;
public SimpleRemote(Command command) {
this.command = command;
}
public void buttonPressed() {
command.execute();
}
}
Note: The simple remote which invokes the command is called as the Invoker.
Let us tie all these together,
public class SimpleRemoteMain {
public static void main(String[] args) {
Light light = new Light(); //Receiver
Command command = new LightOnCommand(light); //Command object
SimpleRemote simpleRemote = new SimpleRemote(command); //Invoker
simpleRemote.buttonPressed();
}
}
Prints,
Switching on the light
The sequence of interactions between the participants are shown below.
- The Client creates the receiver and the command object.
- Client passes the command object to the invoker.
- Later, the client asks the invoker to do some action (pressing the button in our example).
- The invoker calls the execute() method on the command object.
- The command takes care of the rest by calling one or more methods on the receiver object.
Home automation remote using the command pattern
Let us now use the command pattern and update our initial remote solution for home automation.
Creating the command objects
We will create the rest of the command classes for our use-case seen earlier. The LightOffCommand has a Light object on which it calls the off() method.
public class LightOffCommand implements Command {
private final Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
}
The StereoOnCommand and StereoOffCommand are shown below. The StereoOnCommand encapsulates the action of both turning the stereo on and setting the volume.
public class StereoOnCommand implements Command {
private final Stereo stereo;
public StereoOnCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
this.stereo.on();
this.stereo.setVolume(8);
}
}
public class StereoOffCommand implements Command {
private final Stereo stereo;
public StereoOffCommand(Stereo stereo) {
stereo = stereo;
}
@Override
public void execute() {
stereo.off();
}
}
The commands related to operating the ceiling fan are,
public class FanHighCommand implements Command {
private final CeilingFan ceilingFan;
public FanHighCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
ceilingFan.setSpeedToHigh();
}
}
public class FanMediumCommand implements Command {
private final CeilingFan ceilingFan;
public FanMediumCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
ceilingFan.setSpeedToMedium();
}
}
public class FanLowCommand implements Command {
private final CeilingFan ceilingFan;
public FanLowCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
ceilingFan.setSpeedToLow();
}
}
public class FanOffCommand implements Command {
private final CeilingFan ceilingFan;
public FanOffCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
ceilingFan.turnOff();
}
}
Implementing the remote control using commands
The updated remote control class (Invoker) has a list of commands. The commands are built and passed as per the positioning of the button slots in the remote.
Example: When slot 0 is pressed, it gets the first command from the list and calls the execute() method on it. That is it!! The remote has no idea who performs the action or how it would be done.
// Using commands
public class RemoteControlV2 {
private final List<Command> commands;
public RemoteControlV2(List<Command> commands) {
this.commands = List.copyOf(commands);
}
public void buttonPressed(int slotNum) {
if (slotNum < 0 || slotNum >= commands.size()) {
throw new IllegalArgumentException("Invalid button slot number: " + slotNum);
}
commands.get(slotNum).execute();
}
}
Now, for the last part, let us create the device classes (receivers) and the command objects. Next, we pass the commands to the remote controller class.
public class RemoteControlV2Main {
public static void main(String[] args) {
Light light = new Light();
Stereo stereo = new Stereo();
CeilingFan ceilingFan = new CeilingFan();
LightOnCommand lightOnCommand = new LightOnCommand(light);
LightOffCommand lightOffCommand = new LightOffCommand(light);
StereoOnCommand stereoOnCommand = new StereoOnCommand(stereo);
StereoOffCommand stereoOffCommand = new StereoOffCommand(stereo);
FanHighCommand fanHighCommand = new FanHighCommand(ceilingFan);
FanMediumCommand fanMediumCommand = new FanMediumCommand(ceilingFan);
FanLowCommand fanLowCommand = new FanLowCommand(ceilingFan);
FanOffCommand fanOffCommand = new FanOffCommand(ceilingFan);
List<Command> commands = List.of(
lightOnCommand, lightOffCommand,
stereoOnCommand, stereoOffCommand,
fanLowCommand, fanMediumCommand, fanHighCommand, fanOffCommand
);
RemoteControlV2 remoteControlV2 = new RemoteControlV2(commands);
// Controlling the light
remoteControlV2.buttonPressed(0);
remoteControlV2.buttonPressed(1);
System.out.println();
// Controlling the stereo
remoteControlV2.buttonPressed(2);
remoteControlV2.buttonPressed(3);
System.out.println();
// Controlling the fan
remoteControlV2.buttonPressed(4);
remoteControlV2.buttonPressed(5);
remoteControlV2.buttonPressed(6);
remoteControlV2.buttonPressed(7);
}
}
The invocation on the remote controller is the same as before and we get the same output as before.
Switching on the light
Switching off the light
Turning on the stereo
Setting the volume of stereo to 8
Turning off the stereo
Setting fan speed to low
Setting fan speed to medium
Setting fan speed to high
Turning off the fan
Observation
With the introduction of the command pattern, we have rewritten the remote controller logic using it. Let us revisit the problems we had in our naive implementation.
Since the remote controller is decoupled from the device classes, when we add a new operation on the device class (new fan speed setting), then we can easily add a new command object and pass it to the remote controller. Thus, the remote controller is not affected by it. This adheres to the open-closed design principle (open for extension and closed for modification).
Similarly, if we have a new device added, we will bring in the new device class into our application and create command objects to control them. For example, if we want to control the Garage, we will create GarageOpenCommand and GarageCloseCommand etc., Again, the remote controller is not affected by this.
Finally, the remote has zero knowledge of the device classes.
Structure (class diagram) of Command Pattern
The class diagram for the Command Pattern is:
Conclusion
We learnt about the Command Design Pattern in this post. We started with the use-case of controlling the home automation devices without using the command pattern. Then we learnt the command pattern and applied it to the home automation devices so that the invoker (remote) is decoupled from the home device classes.
While you are here, take a look at other design-patterns as well.