← Back to list

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…

Swapnil agrawal · 2026-01-18 20:25 · 0 claps · 17.1 min read paywalled
#command-pattern #low-level-design #design-patterns #object-oriented-design #java
Open on Medium ↗
Wiki topics: 🥊 · Combat Sports

Pattern Recognition #6: Command Pattern

(Gemini Generated)

(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:

  1. Tight Coupling: Your RemoteControl knows about every single device. It’s tightly coupled to Light , CeilingFan , Stereo , etc.
  2. Violates Open/Closed Principle: Every time you add a new device or change an assignment, you have to modify the RemoteControl class.
  3. Hard to Extend: Want to add features like:
  • Logging what commands were executed?
  • Queuing commands to execute later?
  • Saving favorite configurations?
  1. No Undo/Redo: How do you know what the last action was and how to reverse it? With this design, it’s nearly impossible.

  2. 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)
  • setCommand lets 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:

  1. Decoupling: The remote doesn’t know anything about lights, fans, or stereos. It only knows about the Command interface!
  2. 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
  3. Flexibility: We can change slot assignments at runtime. Customers can reprogram their remote!
  4. 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:

  1. Add an undo() method to the Command interface
  2. Implement the above method in each concrete command
  3. 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