Command pattern — encapsulating requests as objects
Intermediate5 min read·eng-04-003
interviewlaravel-srccompare
Concept
Command pattern encapsulates a request as an object, allowing parameterization, queuing, logging, and undoable operations. The invoker doesn't know HOW the command is executed — just that it CAN be executed.
Structure:
- Command Interface:
execute(): void(and optionallyundo(): void). - Concrete Command: Implements execute. Stores the receiver and parameters.
- Receiver: The object that knows HOW to do the work (e.g.,
TextEditor,Database). - Invoker: Triggers the command. Doesn't know about the receiver. May queue or log commands.
- Client: Creates the command object, sets receiver and parameters, passes to invoker.
Benefits:
- Decouple sender from receiver: The invoker doesn't know what the command does.
- Undo/Redo:
execute()+undo()methods. Store history of commands. - Queuing: Commands can be serialized and queued for later execution.
- Logging: Log all executed commands for audit trails.
- Macro commands: A command that contains multiple sub-commands.
Laravel connection: php artisan queue:push queues jobs. Jobs ARE the Command pattern. Artisan::call() is an invoker. The artisan Command class is a concrete command.
Command vs Strategy: Both encapsulate behavior in objects. Strategy defines HOW to do something (algorithm selection). Command defines WHAT to do (an action that can be queued, logged, undone).
Code Example
php
<?php
// Command Interface
interface CommandInterface
{
public function execute(): void;
public function undo(): void;
}
// Receiver — knows how to do the actual work
class TextEditor
{
private string $text = '';
public function insertText(string $text, int $position): void
{
$this->text = substr_replace($this->text, $text, $position, 0);
}
public function deleteText(int $position, int $length): string
{
$deleted = substr($this->text, $position, $length);
$this->text = substr_replace($this->text, '', $position, $length);
return $deleted;
}
public function getText(): string { return $this->text; }
}
// Concrete Commands
class InsertTextCommand implements CommandInterface
{
public function __construct(
private readonly TextEditor $editor,
private readonly string $text,
private readonly int $position,
) {}
public function execute(): void { $this->editor->insertText($this->text, $this->position); }
public function undo(): void { $this->editor->deleteText($this->position, strlen($this->text)); }
}
class DeleteTextCommand implements CommandInterface
{
private string $deletedText = '';
public function __construct(
private readonly TextEditor $editor,
private readonly int $position,
private readonly int $length,
) {}
public function execute(): void { $this->deletedText = $this->editor->deleteText($this->position, $this->length); }
public function undo(): void { $this->editor->insertText($this->deletedText, $this->position); }
}
// Invoker — maintains history for undo/redo
class CommandHistory
{
private array $history = [];
private int $pointer = -1;
public function execute(CommandInterface $command): void
{
$command->execute();
// Clear redo history (anything after current pointer)
$this->history = array_slice($this->history, 0, $this->pointer + 1);
$this->history[] = $command;
$this->pointer++;
}
public function undo(): void
{
if ($this->pointer < 0) return;
$this->history[$this->pointer]->undo();
$this->pointer--;
}
public function redo(): void
{
if ($this->pointer + 1 >= count($this->history)) return;
$this->pointer++;
$this->history[$this->pointer]->execute();
}
}
// Usage
$editor = new TextEditor();
$history = new CommandHistory();
$history->execute(new InsertTextCommand($editor, 'Hello', 0));
$history->execute(new InsertTextCommand($editor, ' World', 5));
echo $editor->getText(); // "Hello World"
$history->undo();
echo $editor->getText(); // "Hello"
$history->redo();
echo $editor->getText(); // "Hello World"