Skip to content

命令模式(Command)

前言

命令模式将请求封装成对象,使请求可以参数化、队列化、记录日志和支持撤销/重做。它是实现撤销功能、宏命令、任务队列的经典模式。本文将详细讲解命令模式的核心原理及实际应用。


一、核心概念

1.1 定义

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

1.2 核心角色

角色说明
命令接口(Command)声明执行和撤销方法
具体命令(ConcreteCommand)实现命令,持有接收者引用
接收者(Receiver)执行实际工作
调用者(Invoker)持有命令,触发执行
客户端(Client)创建命令并组装

二、代码实现

2.1 基础实现

php
<?php
// 命令接口
interface Command
{
    public function execute(): void;
    public function undo(): void;
}

// 接收者:执行实际操作
class Light
{
    public function turnOn(): void
    {
        echo "💡 Light is ON" . PHP_EOL;
    }

    public function turnOff(): void
    {
        echo "💡 Light is OFF" . PHP_EOL;
    }
}

// 具体命令:开灯
class TurnOnCommand implements Command
{
    public function __construct(private Light $light) {}

    public function execute(): void
    {
        $this->light->turnOn();
    }

    public function undo(): void
    {
        $this->light->turnOff();
    }
}

// 具体命令:关灯
class TurnOffCommand implements Command
{
    public function __construct(private Light $light) {}

    public function execute(): void
    {
        $this->light->turnOff();
    }

    public function undo(): void
    {
        $this->light->turnOn();
    }
}

// 调用者:遥控器
class RemoteControl
{
    /** @var Command[] */
    private array $history = [];

    public function executeCommand(Command $cmd): void
    {
        $cmd->execute();
        $this->history[] = $cmd;
    }

    public function undo(): void
    {
        $cmd = array_pop($this->history);
        if ($cmd !== null) {
            $cmd->undo();
        }
    }
}

// 使用
$light = new Light();
$remote = new RemoteControl();

$turnOn = new TurnOnCommand($light);
$turnOff = new TurnOffCommand($light);

$remote->executeCommand($turnOn); // 💡 Light is ON
$remote->executeCommand($turnOff); // 💡 Light is OFF
$remote->undo(); // 💡 Light is ON(撤销关灯)
$remote->undo(); // 💡 Light is OFF(撤销开灯)

2.2 实际案例:文本编辑器

php
<?php
// 命令接口
interface EditorCommand
{
    public function execute(): void;
    public function undo(): void;
}

// 接收者:文本编辑器
class TextEditor
{
    private string $content = "";

    public function insert(string $text): void
    {
        $this->content .= $text;
    }

    public function delete(int $length): string
    {
        $deleted = substr($this->content, -$length);
        $this->content = substr($this->content, 0, -$length);
        return $deleted;
    }

    public function getContent(): string
    {
        return $this->content;
    }
}

// 插入命令
class InsertCommand implements EditorCommand
{
    private string $text;

    public function __construct(
        private TextEditor $editor,
        string $text
    ) {
        $this->text = $text;
    }

    public function execute(): void
    {
        $this->editor->insert($this->text);
    }

    public function undo(): void
    {
        $this->editor->delete(strlen($this->text));
    }
}

// 删除命令
class DeleteCommand implements EditorCommand
{
    private string $deletedText = "";

    public function __construct(
        private TextEditor $editor,
        private int $length
    ) {}

    public function execute(): void
    {
        $this->deletedText = $this->editor->delete($this->length);
    }

    public function undo(): void
    {
        $this->editor->insert($this->deletedText);
    }
}

// 调用者:命令历史管理
class CommandManager
{
    /** @var EditorCommand[] */
    private array $undoStack = [];
    /** @var EditorCommand[] */
    private array $redoStack = [];

    public function execute(EditorCommand $cmd): void
    {
        $cmd->execute();
        $this->undoStack[] = $cmd;
        $this->redoStack = []; // 清空重做栈
    }

    public function undo(): void
    {
        $cmd = array_pop($this->undoStack);
        if ($cmd !== null) {
            $cmd->undo();
            $this->redoStack[] = $cmd;
        }
    }

    public function redo(): void
    {
        $cmd = array_pop($this->redoStack);
        if ($cmd !== null) {
            $cmd->execute();
            $this->undoStack[] = $cmd;
        }
    }
}

// 使用
$editor = new TextEditor();
$manager = new CommandManager();

$manager->execute(new InsertCommand($editor, "Hello "));
$manager->execute(new InsertCommand($editor, "World"));
echo $editor->getContent() . PHP_EOL; // Hello World

$manager->execute(new DeleteCommand($editor, 5));
echo $editor->getContent() . PHP_EOL; // Hello

$manager->undo(); // 撤销删除
echo $editor->getContent() . PHP_EOL; // Hello World

$manager->redo(); // 重做删除
echo $editor->getContent() . PHP_EOL; // Hello

2.3 宏命令

php
<?php
// 宏命令:组合多个命令
class MacroCommand implements Command
{
    /** @var Command[] */
    private array $commands = [];

    public function addCommand(Command $cmd): void
    {
        $this->commands[] = $cmd;
    }

    public function execute(): void
    {
        foreach ($this->commands as $cmd) {
            $cmd->execute();
        }
    }

    public function undo(): void
    {
        // 逆序撤销
        foreach (array_reverse($this->commands) as $cmd) {
            $cmd->undo();
        }
    }
}

// 使用:一键执行多个操作
$light = new Light();
$macro = new MacroCommand();
$macro->addCommand(new TurnOnCommand($light));
$macro->addCommand(new TurnOffCommand($light));
$macro->addCommand(new TurnOnCommand($light));

$macro->execute(); // 开 → 关 → 开
$macro->undo(); // 关 → 开 → 关

三、适用场景

场景说明
撤销/重做编辑器、绘图工具
任务队列异步任务排队执行
宏命令批量执行多个操作
事务操作全部成功或全部回滚
日志/审计记录所有操作
UI 按钮按钮触发命令

四、优缺点分析

优点缺点
支持撤销/重做需要创建多个命令类
请求队列化增加代码复杂度
日志记录可能过度设计
解耦调用者和接收者命令对象可能持有大量状态
支持宏命令撤销栈可能占用内存

五、常见踩坑与问题排查

5.1 撤销栈无限增长

php
<?php
// 问题:命令历史无限增长
class BadManager
{
    /** @var Command[] */
    private array $history = [];

    public function execute(Command $cmd): void
    {
        $cmd->execute();
        $this->history[] = $cmd; // 无限增长
    }
}

// 解决:限制历史大小
class GoodManager
{
    /** @var Command[] */
    private array $history = [];
    private int $maxSize = 100;

    public function execute(Command $cmd): void
    {
        $cmd->execute();
        $this->history[] = $cmd;
        if (count($this->history) > $this->maxSize) {
            array_shift($this->history); // 移除最老的
        }
    }
}

5.2 命令状态不完整

php
<?php
// 问题:删除命令未保存被删除的内容,无法撤销
class BadDeleteCommand implements Command
{
    public function execute(): void
    {
        // 删除但未保存
    }

    public function undo(): void
    {
        // 无法恢复!
    }
}

// 解决:在 execute 中保存状态
class GoodDeleteCommand implements Command
{
    private string $deletedContent = "";

    public function __construct(private TextEditor $editor, private int $length) {}

    public function execute(): void
    {
        $this->deletedContent = $this->editor->delete($this->length); // 保存
    }

    public function undo(): void
    {
        $this->editor->insert($this->deletedContent); // 恢复
    }
}

六、优化方案与进阶

6.1 函数式命令

php
<?php
// 使用闭包代替命令类
function createInsertCommand(TextEditor $editor, string $text): array
{
    return [
        'execute' => fn() => $editor->insert($text),
        'undo' => fn() => $editor->delete(strlen($text)),
    ];
}

// 使用
$cmd = createInsertCommand($editor, 'Hello');
$cmd['execute'](); // 执行
$cmd['undo']();    // 撤销

6.2 结合责任链

php
<?php
// 命令处理链
class CommandChain
{
    /** @var callable[] */
    private array $handlers = [];

    public function addHandler(callable $handler): void
    {
        $this->handlers[] = $handler;
    }

    public function execute(Command $cmd): bool
    {
        foreach ($this->handlers as $handler) {
            if ($handler($cmd)) return true;
        }
        $cmd->execute();
        return true;
    }
}

七、全文总结

命令模式的核心是 将请求封装为对象,支持撤销、重做、队列化和宏命令

核心要点

  1. 命令封装请求,解耦调用者和接收者
  2. 撤销/重做需在 execute 中保存状态
  3. 宏命令组合多个命令,批量执行和撤销
  4. 适用于编辑器、任务队列、事务等场景
  5. 可用函数式方式简化命令创建