Mastering the Command Design Pattern with Practical Examples

Nikolay Nikolov
5 min readNov 1, 2023

--

What is it?

The Command Design Pattern is a behavioral design pattern that revolutionizes the way requests are handled in software development. By encapsulating a request as an object, this pattern empowers developers to parameterize clients with diverse requests, efficiently queue and log requests, and seamlessly support undoable operations.

Its elegance lies in providing a flexible framework for orchestrating complex actions without the need for tight coupling.

Where is it used?

The Command Pattern finds its stride in scenarios where the sender and receiver of a request should dance to their own tunes without stepping on each other’s toes. This pattern proves invaluable in systems requiring command queuing, undo/redo functionalities, or the implementation of high-level operations built on a foundation of lower-level actions.

Advantages

- Decouples Sender and Receiver:
— Liberates the sender from the burden of knowing the intricate details of the receiver’s implementation.

- Supports Undo/Redo Operations:
— Commands gracefully lend themselves to reversal, allowing for seamless execution in reverse order, thus enabling undo and redo functionalities.

- Flexibility and Extensibility:
— Welcomes the addition of new commands without unsettling existing client code, championing the open/closed principle.

- Command Queuing:
— Commands play well in queues, offering an elegant solution for implementing features like job scheduling.

Example

In the following examples, we first explore a poorly designed scenario that lacks separation of concerns and violates key principles of good software design. Subsequently, we showcase a refactored version using the Command Pattern, addressing the identified issues.

class Illuminator 
{
public function illuminate(): void {
echo "Light is on.\n";
}

public function darken(): void {
echo "Light is off.\n";
}
}

class RemoteSwitch
{
public function __construct(private Illuminator $illuminator) {}

public function operate(string $operation): void {
match ($operation) {
'on' => $this->illuminator->illuminate(),
'off' => $this->illuminator->darken(),
default => throw new RuntimeException(
sprintf('Unknown operation %s', $operation)
),
};
}
}

// Usage
$illuminator = new Illuminator();
$switch = new RemoteSwitch($illuminator);
$switch->operate('on');

In the provided code, there are potential disadvantages and considerations:

1. Tight Coupling:
— The RemoteSwitch class is tightly coupled to the Illuminator class, as it directly depends on its methods (illuminate and darken). While this is a common characteristic of the Command Pattern, it can limit the ability to substitute different illuminator implementations without modifying the RemoteSwitch class.

2. Limited Command Abstraction:
— The RemoteSwitch class currently directly exposes operations like illuminate and darken, which might not fully embrace the abstraction provided by the Command Pattern.

3. Lack of Undo/Redo Support:
— The current implementation lacks built-in support for undo and redo operations. If these functionalities become essential, additional design considerations and modifications to the code may be necessary.

4. Potential State Management Challenges:
— The code does not address potential state management concerns. For example, if the Illuminator has a more complex internal state, the RemoteSwitch might need additional logic to handle and maintain that state.

5. Limited Extensibility:
— While the code is more modular than the initial example, adding new operations or commands still involves modifying the existing code in the RemoteSwitch class. Depending on the system's complexity, this could lead to issues in maintaining the Open/Closed principle.

6. Single Responsibility Principle (SRP):
— The RemoteSwitch class still handles both command execution and decision-making logic. Further refactoring could separate these responsibilities more explicitly.

After we discussed the disadvantages of the poorly constructed code, let’s have a look at a refactored code leveraging the Command Pattern.

class Illuminator 
{
public function illuminate(): void {
echo "Light is on.\n";
}

public function darken(): void {
echo "Light is off.\n";
}
}

interface Command
{
public function execute(): void;
}

class LightOnCommand implements Command
{
public function __construct(private Illuminator $illuminator) {}

public function execute(): void {
$this->illuminator->illuminate();
}
}

class LightOffCommand implements Command
{
public function __construct(private Illuminator $illuminator) {}

public function execute(): void {
$this->illuminator->darken();
}
}

class RemoteSwitch
{
private Command $command;

public function setCommand(Command $command) {
$this->command = $command;
}

public function pressButton(): void {
$this->command->execute();
}
}

// Usage
$illuminator = new Illuminator();
$lightOn = new LightOnCommand($illuminator);
$lightOff = new LightOffCommand($illuminator);

$remote = new RemoteSwitch();

$remote->setCommand($lightOn);
$remote->pressButton(); // Light is on.

$remote->setCommand($lightOff);
$remote->pressButton(); // Light is off.

The refactored code using the Command Pattern offers several advantages:

1. Decoupling of Components:
— The Command pattern decouples the sender (RemoteSwitch) from the receiver (Illuminator). The RemoteSwitch is no longer directly tied to the concrete implementation of the Illuminator, promoting a more flexible and maintainable design.

2. Extensibility:
— New commands, such as LightOnCommand and LightOffCommand, can be added without modifying existing client code. This adheres to the Open/Closed principle, allowing the system to grow with new functionalities without altering the existing codebase.

3. Command Abstraction:
— The introduction of the Command interface provides a clear abstraction for commands. This abstraction allows for a variety of commands to be created, each encapsulating a specific operation, making the system more modular and adaptable.

4. Support for Undo/Redo:
— The Command pattern inherently supports undo and redo operations. While not explicitly demonstrated in the provided code, the structure allows for the easy implementation of such features by maintaining a history of executed commands.

5. Enhanced Testability:
— The code is more testable due to the use of interfaces and dependency injection. Unit testing can be performed by creating mock implementations of the Illuminator and Command interfaces, facilitating a more controlled and predictable testing environment.

6. Separation of Concerns:
— The RemoteSwitch class is now responsible only for invoking commands, adhering to the Single Responsibility Principle (SRP). The decision of which command to execute and the implementation details of the command are separated, improving code organization and readability.

7. Clear Command Structure:
— Each command class (LightOnCommand and LightOffCommand) encapsulates a specific operation, leading to a more modular and understandable code structure. This separation of responsibilities makes the codebase easier to comprehend and maintain.

8. Easy Replacement of Commands:
— Commands can be easily replaced or extended without affecting the RemoteSwitch class. This flexibility allows for dynamic reconfiguration of the system, accommodating changes in behavior or the introduction of new features.

In summary, the refactored code using the Command Pattern exhibits advantages such as decoupling, extensibility, support for undo/redo, enhanced testability, separation of concerns, a clear command structure, and ease of command replacement. These benefits contribute to a more robust and adaptable design.

For further exploration, you can also delve into my other articles, such as “Mastering the Factory Pattern with Practical Examples” or “Mastering the Builder Design Pattern with Practical Examples”.

If you enjoyed this, subscribe to my future articles, follow me if you like 🚀

Clap 👏🏻, drop a comment 💬, and share this article with anyone you think would find it valuable.

Thank you for your support!

Happy coding!

--

--

Nikolay Nikolov

Head of Software Development at CONUTI GmbH | 20 years experience | Passionate about clean code, design patterns, and software excellence.