Command

Behavioral Design Patterns

Imagine you are developing a "smart home" application with a UI that includes buttons to control various devices like lights, stereos, and garage doors.

A naive approach would be to code the logic directly inside each button's event listener:

public class SmartHomeUI {
    private Light livingRoomLight = new Light();
    private Stereo mainStereo = new Stereo();

    public void onLightOnButtonClick() {
        livingRoomLight.on();
    }

    public void onStereoOnButtonClick() {
        mainStereo.on();
        mainStereo.setCd();
        mainStereo.setVolume(11);
    }
    // ... and so on
}

This UI class is now tightly coupled to the Light and Stereo classes. What happens when you want to add a Television? You have to modify the SmartHomeUI class. What if you want to reuse these actions in a different context, like a voice-activated assistant? You'd have to duplicate the logic.

Furthermore, how would you implement a global "undo" button? It's nearly impossible because the UI has no record of the actions it has performed; it just called methods directly.

Encapsulating a Request as an Object

The Command is a behavioral design pattern that turns a request into a stand-alone object containing all information about the request. This object, the "command," serves as a decoupling layer between the object that invokes an operation and the object that knows how to perform it.

The invoker (e.g., a button) doesn't call a method on the receiver (e.g., a light) directly. Instead, it calls an execute() method on the command object. The command object itself holds a reference to the receiver and knows which of the receiver's methods to call. This encapsulation allows you to parameterize objects with operations, queue requests, and, most famously, support undoable operations.

The Participants

  1. Command: The interface for all command objects. It typically declares a single method, execute(), and often an undo() method.
  2. Concrete Command: A specific implementation of the Command interface. It holds a reference to a Receiver object and knows how to trigger the correct action on it.
  3. Receiver: The object that performs the actual work (e.g., the Light class). It has the business logic.
  4. Invoker: The object that triggers the command (e.g., a RemoteControl button). It holds a Command object and calls its execute() method.
  5. Client: The application that wires everything together: it creates the receivers, creates the concrete commands, and associates them with invokers.

Example: A Smart Remote Control

Let's build a simple remote control using the Command pattern.

// The Command interface
public interface ICommand {
    void execute();
    void undo();
}

// The Receiver class
public class Light {
    public void on() { System.out.println("Light is ON"); }
    public void off() { System.out.println("Light is OFF"); }
}

// A Concrete Command to turn the light on
public class LightOnCommand implements ICommand {
    private final Light light;

    public LightOnCommand(Light light) { this.light = light; }

    @Override
    public void execute() {
        light.on();
    }

    @Override
    public void undo() {
        light.off(); // The undo operation for "on" is "off"
    }
}

// A Concrete Command to turn the light off
public class LightOffCommand implements ICommand {
    private final Light light;

    public LightOffCommand(Light light) { this.light = light; }

    @Override
    public void execute() {
        light.off();
    }

    @Override
    public void undo() {
        light.on();
    }
}


// The Invoker class
public class RemoteControl {
    private ICommand command;

    public void setCommand(ICommand command) {
        this.command = command;
    }

    public void onButtonWasPushed() {
        command.execute();
    }
    
    public void onUndoButtonWasPushed() {
        command.undo();
    }
}

// The Client, which wires everything up
public class Client {
    public static void main(String[] args) {
        RemoteControl remote = new RemoteControl();
        Light livingRoomLight = new Light();

        // Create commands and associate them with the receiver
        ICommand lightOn = new LightOnCommand(livingRoomLight);
        ICommand lightOff = new LightOffCommand(livingRoomLight);

        // Turn the light on
        remote.setCommand(lightOn);

        System.out.println("--- Pushing ON button ---");
        remote.onButtonWasPushed(); // Output: Light is ON

        // Press undo
        System.out.println("--- Pushing UNDO button ---");
        remote.onUndoButtonWasPushed(); // Output: Light is OFF
    }
}

Interview Focus: Analysis and Trade-offs

Benefits:

  • Decoupling: The key benefit. The RemoteControl (Invoker) is completely decoupled from the Light (Receiver). The remote could be reprogrammed to control a Stereo or a GarageDoor just by giving it a different command object, without changing its code.
  • Commands as First-Class Objects: Since requests are now objects, they can be manipulated like any other object: stored in a list, passed as method parameters, queued, logged, etc.
  • Undo/Redo Functionality: The pattern provides a clean and logical structure for implementing undo and redo operations. Each command object knows how to reverse its own action.
  • Composition: You can assemble a sequence of commands into a single MacroCommand to execute them as a single batch operation.

Drawbacks:

  • Increased Complexity: The primary trade-off is an increase in the number of classes. For every simple action, you need to create a whole new ConcreteCommand class, which can lead to a lot of boilerplate code.

How Command Relates to Other Concepts

  • Object-Oriented Callbacks: The Command pattern can be seen as an object-oriented replacement for callback functions. Instead of passing a function pointer to the invoker, you pass a command object.
  • Queuing and Asynchronous Operations: Because commands are objects, they are perfect for queuing. You can add command objects to a queue, and a pool of worker threads can process them asynchronously. This is fundamental to job/task processing systems.
  • Composite Pattern: A MacroCommand that executes a sequence of other commands is a direct application of the Composite pattern. The invoker can treat the macro (a composite) the same way it treats a single command (a leaf).
  • Transactional Behavior: A sequence of commands can be wrapped in a transaction. If one command in the sequence fails, you can iterate through the history of executed commands and call their undo() methods to roll back the entire transaction.