0

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 optionally undo(): 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"