命令模式(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; // Hello2.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;
}
}七、全文总结
命令模式的核心是 将请求封装为对象,支持撤销、重做、队列化和宏命令。
核心要点:
- 命令封装请求,解耦调用者和接收者
- 撤销/重做需在 execute 中保存状态
- 宏命令组合多个命令,批量执行和撤销
- 适用于编辑器、任务队列、事务等场景
- 可用函数式方式简化命令创建
