Pattern Recognition #6: Command Pattern
Welcome back to our design patterns journey. In our previous journey, we learned to control the behavior of objects, how to create objects…
Pattern Recognition #6: Command Pattern

(Gemini Generated)
Welcome back to our design patterns journey. In our previous journey, we learned to control the behavior of objects, how to create objects, and how to ensure only one exists. But what happens when you want to separate the object that invokes a request from the object that actually knows how to perform it? That’s where we are going to learn one of the perfect patterns — the Command Pattern.
But before we jump into theory, let me ask you something: Have you ever worked on a project where pressing a button needs to do different things at different times? Or were you required to add “undo” functionality? Or maybe. You wanted to queue up a series of actions to execute later?
If you’re not a Medium member, you can still read the full article by following this friend link. I hope you enjoy it :)
If you’ve faced any of these scenarios and felt like your code was becoming a tangled mess of if-else statements and tight coupling, you’re in the right place. The Command Pattern is going to change how you think about handling requests in your applications.
The Problem: Home Automation Scenario
The example is understood from the book: Head of First Design Pattern.
Imagine this: You have to build a universal remote control that can control all these devices:
- Lights (Living Room, Kitchen, Bathroom)
- Ceiling Fans (Different speeds)
- Stereo Systems (Play, Stop, Volume)
- Garage Door (Open, Close)
The remote has 7 slots, each with an ON button and an OFF button. Plus, there’s an UNDO button to reverse the last action.
Your remote should be highly flexible. Customers should be able to:
- Assign any device to any slot
- Change assignments anytime
- Support new devices without changing the remote’s code
- Use the UNDO button to reverse any action
The Naive Approach: Let’s Hard-Code Everything
When you’re new to programming (or in a bit hurry, even if you’re experienced), the most obvious solution that comes to mind is:
I'll create a RemoteControl class that knows about all the devices and calls their methods directly!
So you start coding as follows:
public class RemoteControl {
Light livingRoomLight;
Light kitchenLight;
CeilingFan ceilingFan;
Stereo stereo;
GarageDoor garageDoor;
public RemoteContro(Light livingRoomLight, Light kitchenLight,
CeilingFan ceilingFan, Stereo stereo,
GarageDoor garageDoor) {
this.livingRoomLight = livingRoomLight;
this.kitchenLight = kitchenLight;
this.ceilingFans = ceilingFans;
this.stereo = stereo;
this.garageDoor = garageDoor;
}
// Slot 0: Living room Light
public void onButtonSlot0Pressed(){
livingRoomLight.on();
}
public void offButtonSlot0Pressed(){
livingRoomLight.off();
}
// Slot 1: Kitchen Light
public void onButtonSlot1Pressed(){
kitchenLight.on();
}
public void offButtonSlot1Pressed(){
kitchenLight.off();
}
// Slot 2: Ceiling Fan
public void onButtonSlot2Pressed(){
ceilingFan.high();
}
public void offButtonSlot2Pressed(){
ceilingFan.off();
}
// Slot 3: Stereo
public void onButtonSlot3Pressed(){
stereo.on();
stereo.setCD();
stereo.setVolume(11);
}
public void offButtonSlot3Pressed(){
stereo.off();
}
// Slot 4: Garage Door
public void onButtonSlot4Pressed(){
garageDoor.open();
}
public void offButtonSlot4Pressed(){
garageDoor.close();
}
// ... and so on for all 7 slots
public void undoButtonPressed() {
// Um... how do we implement this? 🤔
// We don't know what the last button press was!
}
}
You show this to your senior, feeling pretty confident.
The Conversation: Junior meets Senior
Junior (You): Hey, I’ve finished the remote control implementation! It works perfectly. Each button is mapped to a device.
Senior: Right. And what if we add a new device next month — let’s say, a Smart Curtain?
Junior: I would need to add new methods in the RemoteControl class… and modify existing code…
Senior: I see. And how about that UNDO button? How would you implement it?
Junior: That’s the tricky part. I’d need to track the last button press, maybe with a set of flags. Like lastActionWasLivingRoomLightOn , lastActionWasLivingRoomLightOff … But that gets messy fast with 7 slots and ON/OFF for each…
Senior: Exactly. Let me show you the problems with this approach.
Problems with the Naive Approach:
- Tight Coupling: Your
RemoteControlknows about every single device. It’s tightly coupled toLight,CeilingFan,Stereo, etc. - Violates Open/Closed Principle: Every time you add a new device or change an assignment, you have to modify the
RemoteControlclass. - Hard to Extend: Want to add features like:
- Logging what commands were executed?
- Queuing commands to execute later?
- Saving favorite configurations?
-
No Undo/Redo: How do you know what the last action was and how to reverse it? With this design, it’s nearly impossible.
-
No Macro Commands: What if a customer wants one button to execute multiple actions (like “Movie Mode”: dim lights + turn on TV + close curtains)?
Junior: Wow, I didn’t think about all that. So… what’s the solution?
Senior: Let me show you a different way of thinking about this problem. But first, let me tell you a story about a diner…
The Analogy: The Diner’s Order System
Senior: Think about how a classic diner works. Let me draw this
Without Command Pattern (Chaotic):
// check below mermaid code
graph LR
A[Customer] -->|tells order verbally| B[Waitress]
B -->|remembers & tells| C[Cook]
C -->|prepares| D[Food]

Problems
- Waitress is tightly coupled to both the customer and the cook
- Can’t queue multiple orders easily
- No tracking or logging
- Can’t undo if customer changes mind
With Command Pattern (Smart Way using Order Slips):
graph TB
A[Customer<br/>Client] -->|places order| B[Waitress<br/>Invoker]
B -->|writes| C[Order Slip<br/>Command Object]
C -->|clips to wheel| D[Order Queue]
D -->|next order| E[Cook<br/>Receiver]
E -->|prepares| F[Food]
C -.->|Can Log| G[(Billing System)]
C -.->|Can Undo| H[Cancel/Modify]
C -.->|Can Queue| D

Old Way: Customer -> Waitress -> Cook (Direct, Tight Coupling)
Command Way: Customer -> Waitress -> Order Slip -> Cook (Decoupled via Command)
The Pattern Mapping:
----------------------------------------------------------------------
Diner Component | Command Pattern | Our Remote Problem |
----------------------------------------------------------------------
Customer | Client | Your application |
----------------------------------------------------------------------
Order Slip | Command | LightOnCommand |
----------------------------------------------------------------------
Waitress | Invoker | RemoteControl |
----------------------------------------------------------------------
Cook | Receiver | Light, Fan, Stereo |
----------------------------------------------------------------------
"Order Up" | execute() | command.execute() |
----------------------------------------------------------------------
Cross out order | undo() | command.undo() |
----------------------------------------------------------------------
Here, the order slip (Command) encapsulates:
- What to do (make a burger)
- Who does it (the cook)
- How to undo it (if needed)
The waitress doesn’t need to know HOW to cook — she just passed the order slip
Junior: Ohhh! So instead of the remote directly calling methods on devices, we create ‘command objects’ that know how to call those methods?
Senior: Exactly! The order slip = Command object. Let’s apply this to our remote control problem.
The Solution: Enter the Command Pattern
The Command Pattern says:
Encasulate a request as an object, thereby letting you parameterize clients with different requets, queue or log requests, and support undoable operations.
Let’s break this down into simple terms:
- Encapsulates a request as an object — We turned method calls into Command objects
- Parameterize clients — The RemoteControl works with ANY command; it’s parameterized with different commands
- Queue or log requests — Since commands are objects, we can store them, queue them, log them
- Support undoable operations — Commands know how to undo themselves
The Players:
Role | Description | Diner | Analogy Our Problem
------------------------------------------------------------------------------------------------------------
Command | Interface with execute() | Order slip format | Command interface
ConcreteCommand | Specific command implementation | Specific order (burger order)| LightOnCommand
Receiver | Knows how to do the work | Cook | Light, Fan, Stereo
Invoker | Asks command to execute | Waitress | RemoteControl
Client | Creates commands and sets receivers | Customer | Our app setup code
Building the Solution: Version 1 (Without Undo)
Let’s build this step by step. We’ll start simple and add complexity gradually.
Step 1: Create the Command interface
public interface Command {
void execute();
}
Step 2: Create the Receivers (The Devices)
// Light
public interface ILight {
void turnOn();
void turnOff();
}
public class Light implements ILight {
private final String location;
public Light(String location) {
this.location = location;
}
@Override
public void turnOn() {
System.out.printf("%s light is turned ON\n", this.location);
}
@Override
public void turnOff() {
System.out.printf("%s light is turned OFF\n", this.location);
}
}
// Ceiling Fan
public interface ICeilingFan {
void high();
void medium();
void low();
void off();
}
public class CeilingFan implements ICeilingFan {
private final String location;
public CeilingFan(String location) {
this.location = location;
}
@Override
public void high() {
System.out.printf("%s Ceiling Fan is on High\n", this.location);
}
@Override
public void medium() {
System.out.printf("%s Ceiling Fan is on Medium\n", this.location);
}
@Override
public void low() {
System.out.printf("%s Ceiling Fan is on Low\n", this.location);
}
@Override
public void off() {
System.out.printf("%s Ceiling Fan is Off\n", this.location);
}
}
// Garage Door
public interface IGarageDoor {
void up();
void down();
void stop();
void lightOn();
void lightOff();
}
public class GarageDoor implements IGarageDoor {
private final String location;
public GarageDoor(String location) {
this.location = location;
}
@Override
public void up() {
System.out.printf("%s Door is Open\n", this.location);
}
@Override
public void down() {
System.out.printf("%s Door is Closed\n", this.location);
}
@Override
public void stop() {
System.out.printf("%s Door is Stopped\n", this.location);
}
@Override
public void lightOn() {
System.out.printf("%s Light is On\n", this.location);
}
@Override
public void lightOff() {
System.out.printf("%s Light is Off\n", this.location);
}
}
// Stereo
public interface IStereo {
void on();
void off();
void setCD();
void setDVD();
void setRadio();
void setVolume(int volume);
}
public class Stereo implements IStereo {
private final String location;
public Stereo(String location) {
this.location = location;
}
@Override
public void on() {
System.out.printf("%s Stereo is On\n", this.location);
}
@Override
public void off() {
System.out.printf("%s Stereo is Off\n", this.location);
}
@Override
public void setCD() {
System.out.printf("%s Stereo is set for CD input\n", this.location);
}
@Override
public void setDVD() {
System.out.printf("%s Stereo is set for DVD input\n", this.location);
}
@Override
public void setRadio() {
System.out.printf("%s Stereo is set for Radio input\n", this.location);
}
@Override
public void setVolume(int volume) {
System.out.printf("%s Stereo volume set to %d\n", this.location, volume);
}
}
Step 3: Create Concrete Commands
// Lights
public class LightOnCommand implements Command {
private final ILight light;
public LightOnCommand(ILight light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
public class LightOffCommand implements Command {
private final ILight light;
public LightOffCommand(ILight light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
}
// Ceiling Fan
public class CeilingFanOnCommand implements Command{
private final ICeilingFan ceilingFan;
public CeilingFanOnCommand(ICeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
ceilingFan.high();
}
}
public class CeilingFanOffCommand implements Command{
private final ICeilingFan ceilingFan;
public CeilingFanOffCommand(ICeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
ceilingFan.off();
}
}
// Garage Door
public class GarageDoorOpenCommand implements Command {
private final IGarageDoor garageDoor;
public GarageDoorOpenCommand(IGarageDoor garageDoor) {
this.garageDoor = garageDoor;
}
@Override
public void execute() {
garageDoor.up();
}
}
public class GarageDoorCloseCommand implements Command {
private final IGarageDoor garageDoor;
public GarageDoorCloseCommand(IGarageDoor garageDoor) {
this.garageDoor = garageDoor;
}
@Override
public void execute() {
garageDoor.down();
}
}
// Stereo
public class StereoOnWithCDCommand implements Command{
private final IStereo stereo;
public StereoOnWithCDCommand(IStereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
stereo.on();
stereo.setCD();
stereo.setVolume(11);
}
}
public class StereoOffCommand implements Command{
private final IStereo stereo;
public StereoOffCommand(IStereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
stereo.off();
}
}
Each command implements the Command interface, holds a reference to a receiver (the device), and in execute() , calls methods on the receiver
Step 4: Create a “Do Nothing” Command (Null Object Pattern)
Before we create the remote, we need a smart trick. What if a slot hasn’t been programmed yet? We don’t want null checks everywhere. Solution: Create a Command that does nothing!
public class NoCommand implements Command {
@Override
public void execute() {
System.out.println("No command assigned to this slot.");
}
}
Step 5: Create the Invoker (Remote Control)
public class RemoteControl {
private final Command[] onCommands;
private final Command[] offCommands;
public RemoteControl(int size) {
onCommands = new Command[size];
offCommands = new Command[size];
Command noCommand = new NoCommand();
for (int i = 0; i < size; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void onButtonWasPressed(int slot) {
onCommands[slot].execute();
}
public void offButtonWasPressed(int slot) {
offCommands[slot].execute();
}
public String toString() {
StringBuilder stringBuff = new StringBuilder();
stringBuff.append("\n----- Remote Control -----\n");
for (int i = 0; i < onCommands.length; i++) {
stringBuff.append("[slot ").append(i).append("] ").append(onCommands[i].getClass().getName()).append(" ").append(offCommands[i].getClass().getName()).append("\n");
}
return stringBuff.toString();
}
}
Key Points:
- Remote holds arrays of commands (one ON and one OFF per slot)
setCommandlets us assign commands to slots- When a button is pressed, we just call
execute()on the command - The remote has NO IDEA what commands do! It just knows they implement Command
Step 6: The Client Code (Putting it all together)
public class RemoteLoader {
public static void main(String[] args) {
int totalSlots = 7;
// 1. Invoker
RemoteControl remoteControl = new RemoteControl(totalSlots);
// 2. Receivers
ILight livingRoomLight = new Light("Living Room");
ILight kitchenLight = new Light("Kitchen");
ICeilingFan livingRoomCeilingFan = new CeilingFan("Living Room");
IGarageDoor garageDoor = new GarageDoor("Garage");
IStereo stereo = new Stereo("Living Room");
// 3. Commands
Command livingRoomLightOnCommand = new LightOnCommand(livingRoomLight);
Command livingRoomLightOffCommand = new LightOffCommand(livingRoomLight);
Command kitchenLightOnCommand = new LightOnCommand(kitchenLight);
Command kitchenLightOffCommand = new LightOffCommand(kitchenLight);
Command livingRoomCeilingFanOnCommand = new CeilingFanOnCommand(livingRoomCeilingFan);
Command livingRoomCeilingFanOffCommand = new CeilingFanOffCommand(livingRoomCeilingFan);
Command garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
Command garageDoorCloseCommand = new GarageDoorCloseCommand(garageDoor);
Command stereoOnWithCDCommand = new StereoOnWithCDCommand(stereo);
Command stereoOffCommand = new StereoOffCommand(stereo);
// 4. Setting commands to remote control slots
remoteControl.setCommand(0, livingRoomLightOnCommand, livingRoomLightOffCommand);
remoteControl.setCommand(1, kitchenLightOnCommand, kitchenLightOffCommand);
remoteControl.setCommand(2, livingRoomCeilingFanOnCommand, livingRoomCeilingFanOffCommand);
remoteControl.setCommand(3, garageDoorOpenCommand, garageDoorCloseCommand);
remoteControl.setCommand(4, stereoOnWithCDCommand, stereoOffCommand);
System.out.println(remoteControl);
// 5. Simulating button presses
for (int i = 0; i < totalSlots; i++) {
remoteControl.onButtonWasPressed(i);
remoteControl.offButtonWasPressed(i);
}
}
}
// Output
----- Remote Control -----
[slot 0] Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.LightOnCommand Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.LightOffCommand
[slot 1] Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.LightOnCommand Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.LightOffCommand
[slot 2] Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.CeilingFanOnCommand Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.CeilingFanOffCommand
[slot 3] Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.GarageDoorOpenCommand Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.GarageDoorCloseCommand
[slot 4] Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.StereoOnWithCDCommand Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.StereoOffCommand
[slot 5] Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.NoCommand Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.NoCommand
[slot 6] Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.NoCommand Behavioral_Desing_pattern.Command.SimpleBehaviour.commands.NoCommand
Living Room light is turned ON
Living Room light is turned OFF
Kitchen light is turned ON
Kitchen light is turned OFF
Living Room Ceiling Fan is on High
Living Room Ceiling Fan is Off
Garage Door is Open
Garage Door is Closed
Living Room Stereo is On
Living Room Stereo is set for CD input
Living Room Stereo volume set to 11
Living Room Stereo is Off
No command assigned to this slot.
No command assigned to this slot.
No command assigned to this slot.
No command assigned to this slot.
class Diagram (mermaid code)
classDiagram
class Client {
<<Client>>
RemoteLoader
}
class Command {
<<interface>>
+execute()
}
class NoCommand {
+execute()
}
class LightOnCommand {
-ILight light
+execute()
}
class LightOffCommand {
-ILight light
+execute()
}
class CeilingFanOnCommand {
-ICeilingFan ceilingFan
+execute()
}
class CeilingFanOffCommand {
-ICeilingFan ceilingFan
+execute()
}
class GarageDoorOpenCommand {
-IGarageDoor garageDoor
+execute()
}
class GarageDoorCloseCommand {
-IGarageDoor garageDoor
+execute()
}
class StereoOnWithCDCommand {
-IStereo stereo
+execute()
}
class StereoOffCommand {
-IStereo stereo
+execute()
}
class ILight {
<<interface>>
+turnOn()
+turnOff()
}
class Light {
<<Receiver>>
-String location
+on()
+off()
}
class ICeilingFan{
<<interface>>
+high()
+medium()
+low()
+off()
}
class CeilingFan {
<<Receiver>>
-String location
-int speed
+high()
+medium()
+low()
+off()
}
class IGarageDoor {
<<interface>>
+up()
+down()
+stop()
+lightOn()
+lightOff()
}
class GarageDoor {
<<Receiver>>
-String location
+up()
+down()
+stop()
+lightOn()
+lightOff()
}
class IStereo{
<<interface>>
+on()
+off()
+setCD()
+setVolume(int)
}
class Stereo {
<<Receiver>>
-String location
+on()
+off()
+setCD()
+setVolume(int)
}
class RemoteControl {
<<Invoker>>
-Command[] onCommands
-Command[] offCommands
+setCommand(int, Command, Command)
+onButtonWasPressed(int)
+offButtonWasPressed(int)
}
Client ..> Command : creates
Client ..> RemoteControl : configures
Command <|.. NoCommand : implements
Command <|.. LightOnCommand : implements
Command <|.. LightOffCommand : implements
Command <|.. CeilingFanOnCommand : implements
Command <|.. CeilingFanOffCommand : implements
Command <|.. GarageDoorOpenCommand : implements
Command <|.. GarageDoorCloseCommand : implements
Command <|.. StereoOnWithCDCommand : implements
Command <|.. StereoOffCommand : implements
ILight <|.. Light : implements
ICeilingFan <|.. CeilingFan : implements
IStereo <|.. Stereo : implements
IGarageDoor <|.. GarageDoor : implements
LightOnCommand --> ILight : invokes
LightOffCommand --> ILight : invokes
CeilingFanOnCommand --> ICeilingFan : invokes
CeilingFanOffCommand --> ICeilingFan : invokes
StereoOnWithCDCommand --> IStereo : invokes
StereoOffCommand --> IStereo : invokes
GarageDoorOpenCommand --> IGarageDoor : invokes
GarageDooCloseCommand --> IGarageDoor : invokes
RemoteControl o-- Command : holds array of

Let’s Review: What We’ve Achieved
Junior: Wow! This is much better! Now I see:
- Decoupling: The remote doesn’t know anything about lights, fans, or stereos. It only knows about the
Commandinterface! - Easy to Extend: Want to add a new device? Just: create receiver, create command classes for it, assign them to slots, no need to modify RemoteControl
- Flexibility: We can change slot assignments at runtime. Customers can reprogram their remote!
- Testing: We can test the remote with mock Commands without needing real devices.
But… what about the UNDO button?
Senior: Great Question! That’s where the pattern really shines. Let’s add undo functionality.
Version 2: Adding UNDO Functionality
To support undo, we need to:
- Add an
undo()method to the Command interface - Implement the above method in each concrete command
- Track the last command executed in the remote
Step 1: Update the Command interface
public interface Command {
void execute();
void undo(); // new method for each command
}
Step 2: Update NoCommand
public class NoCommand extends Command {
@Override
public void execute(){}
@Override
public void undo(){} // do nothing
}
Step 3: Update Commands with Simple Undo
public class LightOnCommand implements Command {
// existing code of version 1
@Override
public void undo() {
light.turnOff();
}
}
public class LightOffCommand implements Command {
// existing code of version 1
@Override
public void undo() {
light.turnOn();
}
}
public class GarageDoorOpenCommand implements Command {
// existing code of version 1
@Override
public void undo() {
garageDoor.down();
}
}
public class GarageDoorCloseCommand implements Command {
// existing code of version 1
@Override
public void undo() {
garageDoor.up();
}
}
public class StereoOffCommand implements Command{
// existing code of version 1
@Override
public void undo() {
stereo.on();
stereo.setCD();
stereo.setVolume(11);
}
}
public class StereoOnWithCDCommand implements Command{
// existing code of version 1
@Override
public void undo() {
stereo.off();
}
}
Step 4: Undo with State
For the ceiling fan, it’s not as simple. If the fan was on MEDIUM and we set it to HIGH, undoing should return it to MEDIUM, not just turn it OFF. We need to remember the previous state:
// change in ceilingFan as follows
public interface ICeilingFan {
int HIGH = 3;
int MEDIUM = 2;
int LOW = 1;
int OFF = 0;
void high();
void medium();
void low();
void off();
int getSpeed();
}
// change in CeilingFan class as follows
public class CeilingFan implements ICeilingFan {
private final String location;
private int speed; // state
public CeilingFan(String location) {
this.location = location;
this.speed = OFF; // initial state
}
@Override
public void high() {
speed = HIGH;
System.out.printf("%s Ceiling Fan is on High\n", this.location);
}
@Override
public void medium() {
speed = MEDIUM;
System.out.printf("%s Ceiling Fan is on Medium\n", this.location);
}
@Override
public void low() {
speed = LOW;
System.out.printf("%s Ceiling Fan is on Low\n", this.location);
}
@Override
public void off() {
speed = OFF;
System.out.printf("%s Ceiling Fan is Off\n", this.location);
}
@Override
public int getSpeed() {
return speed;
}
}
// Command
public class CeilingFanHighCommand implements Command{
private final ICeilingFan ceilingFan;
private int prevSpeed; // previous state of ceilingFan
public CeilingFanHighCommand(ICeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.high();
}
@Override
public void undo() {
switch (prevSpeed) {
case ICeilingFan.HIGH:
ceilingFan.high();
break;
case ICeilingFan.MEDIUM:
ceilingFan.medium();
break;
case ICeilingFan.LOW:
ceilingFan.low();
break;
case ICeilingFan.OFF:
default:
ceilingFan.off();
break;
}
}
}
public class CeilingFanOffCommand implements Command{
private final ICeilingFan ceilingFan;
private int prevSpeed; // previous state of ceilingFan
public CeilingFanOffCommand(ICeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.off();
}
@Override
public void undo() {
switch (prevSpeed) {
case ICeilingFan.HIGH:
ceilingFan.high();
break;
case ICeilingFan.MEDIUM:
ceilingFan.medium();
break;
case ICeilingFan.LOW:
ceilingFan.low();
break;
case ICeilingFan.OFF:
default:
ceilingFan.off();
break;
}
}
}
public class CeilingFanMediumCommand implements Command{
private final ICeilingFan ceilingFan;
private int prevSpeed;
public CeilingFanMediumCommand(ICeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.medium();
}
@Override
public void undo() {
switch (prevSpeed) {
case ICeilingFan.HIGH:
ceilingFan.high();
break;
case ICeilingFan.MEDIUM:
ceilingFan.medium();
break;
case ICeilingFan.LOW:
ceilingFan.low();
break;
case ICeilingFan.OFF:
default:
ceilingFan.off();
break;
}
}
}
public class CeilingFanLowCommand implements Command{
private final ICeilingFan ceilingFan;
private int prevSpeed;
public CeilingFanLowCommand(ICeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.low();
}
@Override
public void undo() {
switch (prevSpeed) {
case ICeilingFan.HIGH:
ceilingFan.high();
break;
case ICeilingFan.MEDIUM:
ceilingFan.medium();
break;
case ICeilingFan.LOW:
ceilingFan.low();
break;
case ICeilingFan.OFF:
default:
ceilingFan.off();
break;
}
}
}
Step 5: Update the Remote Control with Undo
public class RemoteControl {
// existing from version 1
private Command undoCommand;
public RemoteControl(int size) {
// existing from version 1
undoCommand = noCommand;
}
public void setCommand(int slot, Command onCommand, Command offCommand) {
// existing from version 1
}
public void onButtonWasPressed(int slot) {
onCommands[slot].execute();
undoCommand = onCommands[slot]; // undo command
}
public void offButtonWasPressed(int slot) {
offCommands[slot].execute();
undoCommand = offCommands[slot]; // undo command
}
public void undoButtonWasPressed() {
undoCommand.undo();
}
public String toString() {
StringBuilder stringBuff = new StringBuilder();
stringBuff.append("\n----- Remote Control -----\n");
for (int i = 0; i < onCommands.length; i++) {
stringBuff.append("[slot ").append(i).append("] ")
.append(onCommands[i].getClass().getSimpleName()).append(" ")
.append(offCommands[i].getClass().getSimpleName()).append("\n");
}
stringBuff.append("[undo] ").append(undoCommand.getClass().getSimpleName()).append("\n");
return stringBuff.toString();
}
}
Step 6: Testing Undo
public class RemoteLoader {
public static void main(String[] args) {
int totalSlots = 7;
// 1. Invoker
RemoteControl remoteControl = new RemoteControl(totalSlots);
// 2. Receivers
ILight livingRoomLight = new Light("Living Room");
ILight kitchenLight = new Light("Kitchen");
ICeilingFan livingRoomCeilingFan = new CeilingFan("Living Room");
IGarageDoor garageDoor = new GarageDoor("Garage");
IStereo stereo = new Stereo("Living Room");
// 3. Commands
Command livingRoomLightOnCommand = new LightOnCommand(livingRoomLight);
Command livingRoomLightOffCommand = new LightOffCommand(livingRoomLight);
Command kitchenLightOnCommand = new LightOnCommand(kitchenLight);
Command kitchenLightOffCommand = new LightOffCommand(kitchenLight);
Command livingRoomCeilingFanHighCommand = new CeilingFanHighCommand(livingRoomCeilingFan);
Command livingRoomCeilingFanMediumCommand = new CeilingFanMediumCommand(livingRoomCeilingFan);
Command livingRoomCeilingFanLowCommand = new CeilingFanLowCommand(livingRoomCeilingFan);
Command livingRoomCeilingFanOffCommand = new CeilingFanOffCommand(livingRoomCeilingFan);
Command garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
Command garageDoorCloseCommand = new GarageDoorCloseCommand(garageDoor);
Command stereoOnWithCDCommand = new StereoOnWithCDCommand(stereo);
Command stereoOffCommand = new StereoOffCommand(stereo);
// 4. Setting commands to remote control slots
remoteControl.setCommand(0, livingRoomLightOnCommand, livingRoomLightOffCommand);
remoteControl.setCommand(1, kitchenLightOnCommand, kitchenLightOffCommand);
remoteControl.setCommand(2, livingRoomCeilingFanHighCommand, livingRoomCeilingFanOffCommand);
remoteControl.setCommand(3, livingRoomCeilingFanMediumCommand, livingRoomCeilingFanOffCommand);
remoteControl.setCommand(4, livingRoomCeilingFanLowCommand, livingRoomCeilingFanOffCommand);
remoteControl.setCommand(5, garageDoorOpenCommand, garageDoorCloseCommand);
remoteControl.setCommand(6, stereoOnWithCDCommand, stereoOffCommand);
System.out.println(remoteControl);
// 5. Testing basic functionality
System.out.println("\n===== Testing Basic Commands =====");
remoteControl.onButtonWasPressed(0); // Living Room Light ON
remoteControl.offButtonWasPressed(0); // Living Room Light OFF
System.out.println(remoteControl);
// 6. Testing Undo functionality
System.out.println("\n===== Testing Undo Functionality =====");
System.out.println("Turning on living room light...");
remoteControl.onButtonWasPressed(0); // Living Room Light ON
System.out.println("Pressing UNDO...");
remoteControl.undoButtonWasPressed(); // Should turn light OFF
System.out.println("\nTurning off living room light...");
remoteControl.offButtonWasPressed(0); // Living Room Light OFF
System.out.println("Pressing UNDO...");
remoteControl.undoButtonWasPressed(); // Should turn light ON
// 7. Testing Ceiling Fan with State
System.out.println("\n===== Testing Ceiling Fan with State =====");
System.out.println("Turning ceiling fan to HIGH...");
remoteControl.onButtonWasPressed(2); // Ceiling Fan HIGH
System.out.println("Pressing UNDO (should go back to OFF)...");
remoteControl.undoButtonWasPressed(); // Should go back to OFF
System.out.println("\nTurning ceiling fan to MEDIUM...");
remoteControl.onButtonWasPressed(3); // Ceiling Fan MEDIUM
System.out.println("Pressing UNDO (should go back to OFF or previous)...");
remoteControl.undoButtonWasPressed(); // Should go back to OFF
System.out.println("\nTurning ceiling fan to LOW...");
remoteControl.onButtonWasPressed(4); // Ceiling Fan LOW
System.out.println("Pressing UNDO (should go back to OFF or previous)...");
remoteControl.undoButtonWasPressed(); // Should go back to OFF
System.out.println("\nTurning ceiling fan OFF from HIGH...");
remoteControl.onButtonWasPressed(2); // Ceiling Fan HIGH
remoteControl.offButtonWasPressed(2); // Ceiling Fan OFF
System.out.println("Pressing UNDO (should go back to HIGH)...");
remoteControl.undoButtonWasPressed(); // Should go back to HIGH
// 8. Testing Garage Door
System.out.println("\n===== Testing Garage Door =====");
System.out.println("Opening garage door...");
remoteControl.onButtonWasPressed(5); // Garage Door UP
System.out.println("Pressing UNDO (should close)...");
remoteControl.undoButtonWasPressed(); // Should close
// 9. Testing Stereo
System.out.println("\n===== Testing Stereo =====");
System.out.println("Turning stereo on with CD...");
remoteControl.onButtonWasPressed(6); // Stereo ON with CD
System.out.println("Pressing UNDO (should turn off)...");
remoteControl.undoButtonWasPressed(); // Should turn OFF
System.out.println("\n===== Final Remote State =====");
System.out.println(remoteControl);
}
}
// Output
----- Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] LightOnCommand LightOffCommand
[slot 2] CeilingFanHighCommand CeilingFanOffCommand
[slot 3] CeilingFanMediumCommand CeilingFanOffCommand
[slot 4] CeilingFanLowCommand CeilingFanOffCommand
[slot 5] GarageDoorOpenCommand GarageDoorCloseCommand
[slot 6] StereoOnWithCDCommand StereoOffCommand
[undo] NoCommand
===== Testing Basic Commands =====
Living Room light is turned ON
Living Room light is turned OFF
----- Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] LightOnCommand LightOffCommand
[slot 2] CeilingFanHighCommand CeilingFanOffCommand
[slot 3] CeilingFanMediumCommand CeilingFanOffCommand
[slot 4] CeilingFanLowCommand CeilingFanOffCommand
[slot 5] GarageDoorOpenCommand GarageDoorCloseCommand
[slot 6] StereoOnWithCDCommand StereoOffCommand
[undo] LightOffCommand
===== Testing Undo Functionality =====
Turning on living room light...
Living Room light is turned ON
Pressing UNDO...
Living Room light is turned OFF
Turning off living room light...
Living Room light is turned OFF
Pressing UNDO...
Living Room light is turned ON
===== Testing Ceiling Fan with State =====
Turning ceiling fan to HIGH...
Living Room Ceiling Fan is on High
Pressing UNDO (should go back to OFF)...
Living Room Ceiling Fan is Off
Turning ceiling fan to MEDIUM...
Living Room Ceiling Fan is on Medium
Pressing UNDO (should go back to OFF or previous)...
Living Room Ceiling Fan is Off
Turning ceiling fan to LOW...
Living Room Ceiling Fan is on Low
Pressing UNDO (should go back to OFF or previous)...
Living Room Ceiling Fan is Off
Turning ceiling fan OFF from HIGH...
Living Room Ceiling Fan is on High
Living Room Ceiling Fan is Off
Pressing UNDO (should go back to HIGH)...
Living Room Ceiling Fan is on High
===== Testing Garage Door =====
Opening garage door...
Garage Door is Open
Pressing UNDO (should close)...
Garage Door is Closed
===== Testing Stereo =====
Turning stereo on with CD...
Living Room Stereo is On
Living Room Stereo is set for CD input
Living Room Stereo volume set to 11
Pressing UNDO (should turn off)...
Living Room Stereo is Off
===== Final Remote State =====
----- Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] LightOnCommand LightOffCommand
[slot 2] CeilingFanHighCommand CeilingFanOffCommand
[slot 3] CeilingFanMediumCommand CeilingFanOffCommand
[slot 4] CeilingFanLowCommand CeilingFanOffCommand
[slot 5] GarageDoorOpenCommand GarageDoorCloseCommand
[slot 6] StereoOnWithCDCommand StereoOffCommand
[undo] StereoOnWithCDCommand
Perfect! The undo works, even for stateful commands like the ceiling fan.
Junior: This is amazing! The undo works perfectly. And I can see how much more flexible this is than my original approach.
Senior: Exactly. But wait — there’s more! There are some advanced features which can be explored,d like
- Macro Commands: a command that executes multiple commands
- Queuing Commands: Since commands are objects, we can queue them
- Logging Commands
- Scheduling Commands
Thank You!
References
- Head First Design Patterns book
- Github code here
메타데이터
- post_id
- 89e9c5cd2f7a
- slug
- pattern-recognition-6-command-pattern-89e9c5cd2f7a
- url
- https://medium.com/@swapnilagarwal2001/pattern-recognition-6-command-pattern-89e9c5cd2f7a
- canonical_url
- https://medium.com/@swapnilagarwal2001/pattern-recognition-6-command-pattern-89e9c5cd2f7a
- author_url
- https://medium.com/@swapnilagarwal2001
- status
- ok
- fetched_at
- 2026-07-19 18:06:16