Design Patterns: Command
Without the Command Pattern, you forgo the flexibility and structure it offers. You end up with tightly coupled classes, harder-to-maintain code, and a lack of extensibility.
Overview
This article demonstrates the use of the Command Pattern to encapsulate a request as an object, allowing you to parameterise methods, delay execution, or manage a queue of requests (e.g., a task scheduler). Each command encapsulates an action and its associated data, making it easy to manage tasks independently.
What problem does it solve?
Say, we have a FileManager
service class. It’s simple enough, it contains a few methods called open()
, save()
and close()
. It manages files locally.
At the moment, the implementation looks a little like this:
$fileManager = new FileManager();
$fileManager->open("example.txt");
$fileManager->save("example.txt");
$fileManager->close("example.txt");
When this was created, it was fine but now there’s a requirement to switch to a CloudStorage
system for some clients, but other clients may need to still use a LocalStorage
system.
The CloudStorage
system comes with a few caveats:
Whenever we communicate with the cloud, we need to
authenticate
the current user to ascertain if they’ve got the relevant permissions.The existing methods (
open
,save
andclose
) potentially may have optional parameters when invoking the cloud storage implementation.Execution between each requires a slight delay when invoking the
CloudStorage
system (the provider isn’t very good), this delay isn’t required if we’re usingLocalStorage
.
We need to accommodate both file systems but we don’t want to change what we’ve got in place for some of the existing clients.
So how do we go about this?
We could adjust the FileManager
service class directly, accommodating for the changes. We could add a parameter to the FileManager
class and pass in a storageType
variable into its constructor.
// Local storage
$storageType = 'local';
$fileManager = new FileManager($storageType);
// Cloud storage
$storageType = 'cloud';
$fileManager = new FileManager($storageType);
Then within the FileManager
class itself, add a few conditional statements to meet the new requirements:
if ($storageType === 'LocalStorage') {
// Do local stuff
} else if ($storageType === 'CloudStorage') {
// Do cloud stuff
}
We can also add a bunch of optional parameters to some methods, such as logging the response from the CloudStorage
API Service:
public function close(string $filename, bool $logError = false): void {
// Close the file.
}
To deal with authentication, we can add a new method to the FileManager
class, even if it isn’t required by the LocalStorage
.
public function authenticate(): void {
// Authenticate the user.
}
You wrap up the project, it works! Well done!
A few months later there’s a need for AmazonStorage
, how do we change things then? Can you see how this FileManager
class starts to become messy and harder to maintain?
Maybe bugs creep in, a few months later there’s another storage system that needs to be integrated. Maybe a storage system becomes deprecated? However, the code is so tightly coupled it becomes a nightmare to remove it.
What if I told you there’s a way to handle each new requirement easily and keep the code flexible, extendable and scalable?
That with each new requirement the complexity doesn’t increase? The time to implement a new storage system doesn’t keep growing and growing with each new requirement? That you could remove a storage system easily?
Let’s look at the Command Pattern.
How do you implement it?
Lets go through it step by step:
1. Define the receivers
The receiver separates command logic from specific actions. It centralises the "how".
Examples include:
Database Operations: Switching between MySQL and SQLite.
File Systems: Supporting local, cloud, or FTP storage.
Notification Systems: Sending emails vs. push notifications.
In this case the receiver is a storage system service class.
interface FileManager
{
public function open(string $fileName): void;
public function save(string $fileName): void;
public function close(string $fileName): void;
}
class LocalStorage implements FileManager
{
public function open(string $fileName): void
{
echo "Opening local file: {$fileName}" . PHP_EOL;
}
public function save(string $fileName): void
{
echo "Saving local file: {$fileName}" . PHP_EOL;
}
public function close(string $fileName): void
{
echo "Closing local file: {$fileName}" . PHP_EOL;
}
}
class CloudStorage implements FileManager
{
public function __construct(private bool $logError = false)
{
// Added cloud storage specific variables.
// Rather than adding new params to existing methods.
}
public function open(string $fileName): void
{
if ($this->authenticate() === false) {
return false;
}
// slight delay here because third party system sucks.
try {
echo "Connecting to cloud storage..." . PHP_EOL;
echo "Opening cloud file: {$fileName}" . PHP_EOL;
} catch (\Exception $ex) {
if ($this->logError == true) {
// Log error
}
}
}
public function save(string $fileName): void
{
if ($this->authenticate() === false) {
return false;
}
// slight delay here because third party system sucks.
try {
echo "Saving file to cloud storage: {$fileName}" . PHP_EOL;
} catch (\Exception $ex) {
if ($this->logError == true) {
// Log error
}
}
}
public function close(string $fileName): void
{
if ($this->authenticate() === false) {
return false;
}
// slight delay here because third party system sucks.
try {
echo "Closing connection for cloud file: {$fileName}" . PHP_EOL;
} catch (\Exception $ex) {
if ($this->logError == true) {
// Log error
}
}
}
public function authenticate(): bool
{
return true; // Obviously this would contain auth logic.
}
}
We’ve created a class for each storage system, LocalStorage
and CloudStorage
. Both implement a FileManager
interface, meaning they both HAVE to contain open
, save
and close
methods.
We’ve moved the current logic into it’s own class called LocalStorage and we’ve accommodated for the specific changes required for the new CloudStorage
system inside a new CloudStorage
class (new parameter, new method and added a delay to accommodate the limitations of the third party storage system).
Now if we ever need to edit or remove a cloud storage system, we’d simply change (or delete) the relevant class.
If we needed to a new one? You guessed it, we add a new class, rather than adjusting an ever-growing if statement.
2. Create a Command Interface
The Command
interface declares a method, often named execute()
. This method is meant to encapsulate a specific operation. The interface sets a contract for concrete command classes, defining the execute()
method that encapsulates the operation to be performed.
interface Command
{
public function execute(): void;
}
3. Create Concrete Command Classes
Concrete commands implement the Command
interface and are responsible for delegating the operation to a specific receiver.
These commands act as intermediaries between the Invoker
and the Receiver
, allowing you to centralise business logic within the receiver while keeping the commands lightweight.
So basically, we’ve creating a Command
class for each method within the Receiver
(i.e LocalStorage
or CloudStorage
).
class OpenFileCommand implements Command
{
public function __construct(
private FileManager $fileManager,
string $fileName
)
{
//
}
public function execute(): void
{
$this->fileManager->open($this->fileName);
}
}
class SaveFileCommand implements Command
{
public function __construct(
private FileManager $fileManager,
string $fileName
)
{
//
}
public function execute(): void
{
$this->fileManager->save($this->fileName);
}
}
class CloseFileCommand implements Command
{
public function __construct(
private FileManager $fileManager,
string $fileName
)
{
//
}
public function execute(): void
{
$this->fileManager->close($this->fileName);
}
}
4. Create an Invoker
The Invoker
is responsible for managing commands. It allows commands to be queued, executed in sequence, or even executed conditionally, making it an essential part of task scheduling or macro systems.
class CommandInvoker
{
private array $commands = [];
public function addCommand(Command $command): void
{
$this->commands[] = $command;
}
public function executeAll(): void
{
foreach ($this->commands as $command) {
$command->execute();
}
$this->commands = [];
}
}
We add commands to the CommandInvoker
then execute them. Once executed, we reset the $commands
array.
We can define the behaviour at run-time, something like this for LocalStorage
:
$fileName = "example.txt";
$fileManager = new LocalStorage();
$openCommand = new OpenFileCommand($fileManager, $fileName);
$saveCommand = new SaveFileCommand($fileManager, $fileName);
$closeCommand = new CloseFileCommand($fileManager, $fileName);
// Use the invoker to queue and execute commands
$invoker = new CommandInvoker();
$invoker->addCommand($openCommand);
$invoker->addCommand($saveCommand);
$invoker->addCommand($closeCommand);
// Execute all queued commands
$invoker->executeAll();
// Output
// Opening local file: example.txt
// Saving local file: example.txt
// Closing local file: example.txt
And maybe something like this for CloudStorage
:
$fileName = "example.txt";
$fileManager = new CloudStorage(logError: true);
$openCommand = new OpenFileCommand($fileManager, $fileName);
$saveCommand = new SaveFileCommand($fileManager, $fileName);
$closeCommand = new CloseFileCommand($fileManager, $fileName);
$invoker = new CommandInvoker();
$invoker->addCommand($openCommand);
$invoker->addCommand($saveCommand);
$invoker->addCommand($closeCommand);
$invoker->executeAll();
// Output
// Connecting to cloud storage...
// Opening cloud file: example.txt
// Saving file to cloud storage: example.txt
// Closing connection for cloud file: example.txt
Since we’ve separated concerns, we can build the behaviours we’d like at run-time without needing to edit one specific class, which will soon grow into an unmanageable ball of mud.
Advantages
Encapsulation of Requests: Each command encapsulates its data and logic, enabling easier modification and maintenance.
Decoupling of Sender and Receiver: The sender (invoker) doesn’t need to know anything about the receiver, promoting loose coupling.
Queueing and Delayed Execution: Commands can be stored and executed later, making them suitable for task scheduling or undo/redo systems.
Extensibility: New commands can be added without modifying existing code.
Disadvantages
Increased Complexity: The pattern introduces multiple classes and abstractions, which might be overkill for simple tasks.
Memory Overhead: Storing commands in a queue may require additional resources, especially for complex or high-frequency tasks.
Summary
Without the Command Pattern, you forgo the flexibility and structure it offers. You end up with tightly coupled classes, harder-to-maintain code, and a lack of extensibility. This approach works fine for small, simple use cases, but as complexity grows, the drawbacks become more apparent. If you anticipate needing additional functionality, such as undo/redo, conditional actions, or the ability to switch between different storage types, using the Command Pattern provides a cleaner, more maintainable solution.
By decoupling the command logic from the receiver, the Command Pattern ensures that the invoker doesn’t need to know the specifics of how an operation is executed, making it easier to add new commands, alter behaviour, and swap implementations without affecting the rest of the application.
Check out the code repository for an actual implementation: Github.