Command Pattern

At its core, the Command Pattern is a behavioral design pattern that transforms a request into a stand-alone object. By encapsulating all the information needed to perform an action—the receiver, the method to call, and the arguments—into a single object, the pattern allows you to parameterize clients with different requests, queue or log requests, and support undoable operations.

In traditional object-oriented programming, a caller typically invokes a method on a receiver directly. While straightforward, this creates a tight coupling between the two. If the logic for the action changes, or if you need to introduce a delay, a queue, or a way to reverse the action, the caller must be modified. The Command Pattern solves this by introducing an abstraction layer, effectively decoupling the object that triggers the command from the object that knows how to execute it.

Core Architecture and Roles

The Command Pattern relies on four primary components to manage the flow of execution:

  • Command Interface: An abstraction that defines the contract for all commands. Typically, this interface declares a method like execute() and, optionally, an undo() method to reverse the operation.
  • Concrete Command: The implementation of the Command interface. It acts as a bridge between the Invoker and the Receiver. It does not perform the business logic itself but instead delegates the work to the Receiver.
  • Invoker: The object responsible for triggering the command. The Invoker does not know the internal details of the operation; it only knows how to call the execute() method on the Command object it holds.
  • Receiver: The object that contains the actual business logic. It is the "worker" that knows how to perform the specific task (e.g., turning on a light or saving a file).

By separating these roles, the system distinguishes between what needs to be done (the Receiver), when it should be done (the Invoker), and how the request is structured (the Command).

Implementation Walkthrough: A Smart Home Example

To illustrate the pattern in action, let's consider a simple light control system. We want a remote control (Invoker) that can turn a light (Receiver) on or off, with the ability to undo the last action.

1. The Receiver

The Light class is our Receiver. It contains the actual logic for changing the state of the hardware.

public class Light {
    public void turnOn() {
        System.out.println("The light is ON");
    }

    public void turnOff() {
        System.out.println("The light is OFF");
    }
}

2. The Command Interface and Concrete Commands

We define a Command interface to ensure all actions are interchangeable. Then, we create specific classes for turning the light on and off.

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

// Concrete Command for turning the light on
public class LightOnCommand implements Command {
    private Light light;

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

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

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

// Concrete Command for turning the light off
public class LightOffCommand implements Command {
    private Light light;

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

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

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

3. The Invoker

The RemoteControl acts as the Invoker. It doesn't care whether it's controlling a light, a fan, or a garage door; it simply triggers the command it has been assigned.

public class RemoteControl {
    private Command command;

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

    public void pressButton() {
        if (command != null) {
            command.execute();
        }
    }

    public void pressUndoButton() {
        if (command != null) {
            command.undo();
        }
    }
}

4. Putting It All Together

The client code assembles the objects and defines the relationship between them.

public class Client {
    public static void main(String[] args) {
        Light livingRoomLight = new Light();
        RemoteControl remote = new RemoteControl();

        // Execute "Turn On"
        Command lightOn = new LightOnCommand(livingRoomLight);
        remote.setCommand(lightOn);
        remote.pressButton(); // Output: The light is ON

        // Execute "Turn Off"
        Command lightOff = new LightOffCommand(livingRoomLight);
        remote.setCommand(lightOff);
        remote.pressButton(); // Output: The light is OFF

        // Undo the last action
        remote.pressUndoButton(); // Output: The light is ON
    }
}

When to Use the Command Pattern

The Command Pattern is particularly powerful in complex systems where flexibility is more important than simplicity. Key use cases include:

  • Undo/Redo Functionality: Because each command is an object, you can store them in a stack. To undo an action, you simply pop the last command and call its undo() method.
  • Operation Queuing and Scheduling: Commands can be added to a queue and executed asynchronously by a background worker or a thread pool, which is essential for handling high-load bursts (load leveling).
  • Macro Recording: By storing a sequence of command objects, you can create "macros" that execute a series of complex steps with a single trigger.
  • Audit Logging: Since every request is encapsulated, you can easily serialize these objects to a log file, allowing the system to recover its state after a crash by replaying the commands.

Analysis: Pros and Cons

Advantages

  • Decoupling: The Invoker and Receiver are completely isolated. This means you can change the Receiver's implementation without touching the Invoker.
  • Extensibility: Adding a new command requires creating a new class. This adheres to the Open/Closed Principle, as you can introduce new functionality without modifying existing code.
  • Composite Commands: You can create a "Macro Command" that contains a list of other commands, allowing you to execute multiple actions as a single unit.

Disadvantages

  • Class Proliferation: Every single operation requires its own class. In a system with hundreds of different actions, this can lead to a "class explosion," making the project structure feel cluttered.
  • Increased Complexity: For simple applications where a direct method call suffices, implementing the Command Pattern can be an over-engineering trap, adding unnecessary boilerplate code.

Summary

The Command Pattern is a sophisticated tool for managing requests. By treating a "request" as a first-class object, it grants developers the ability to manipulate actions as data—storing them, queuing them, and reversing them. While it introduces more classes into the system, the trade-off is a highly flexible, maintainable, and scalable architecture. When deciding whether to implement it, weigh the need for features like undo/redo or asynchronous execution against the overhead of additional boilerplate.