php设计模式之命令模式
模式意图
在软件设计中,我们经常会向一个对象发送请求,但是并不知道接受者是哪个,也不知道被请求的操作有哪些,我们只需要在程序运行时指定具体的请求接受者即可。这个时候,我们就可以用命令模式来将请求者和请求的接受者解耦,让程序设计的更加灵活。
命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。
模式定义
将请求封装成对象,这可以让你使用不同的请求、队列或者日志请求来参数化其他对象。命令模式也可以支持撤销操作。
模式结构
命令模式一般含有以下结构:
- 客户类Client
- 抽象命令类AbstractCommand
- 具体命令类ConcreteCommand
- 调用者Invoker
- 接受者Receiver
模式流程
- 创建接受者对象
new Receiver()
- 创建命令对象,并把接受者对象保存到命令对象中
new ConcreteCommand($receiver)
- 创建调用者对象,并把命令对象保存至调用者对象中
new Invoker($command)
- 执行调用者对象的call()方法,call()方法会调用命令对象的execute()方法,execute()方法会调用接受者的action()方法,action()方法实现具体的功能。
吹电风扇
夏天来了,电风扇和空调是必不可少的。我们可从吹电风扇这个具体案例来说明命令模式。吹电风扇的场景是这样的,人通过操控遥控器来打开或关闭电风扇,人是不用去知道电风扇是如何完成打开或关闭的功能,只需通过遥控器上的按钮即可完成想要的效果。这里面有三个角色,人、遥控器、风扇。在命令模式中,分别对应客户类、调用者、接受者
接受者
我们实现来完成接受者(风扇)的代码,风扇的功能有对风扇的开和关、换挡、模式切换(正常、睡眠)
class FanReceiver
{
const SPEED_OFF = 0;
const SPEED_LOW = 1;
const SPEED_MID = 2;
const SPEED_HIGH = 3;
const MODE_NORMAL = 0;
const MODE_SWING = 1;
private $speed = 0;
private $mode = 0;
public function speed (int $speed = self::SPEED_OFF):void
{
$this->speed = $speed;
switch ($speed) {
case 0:
echo '关闭风扇' . PHP_EOL;
break;
case 1:
echo '切换低速风' . PHP_EOL;
break;
case 2:
echo '切换中速风' . PHP_EOL;
break;
case 3:
echo '切换高速风' . PHP_EOL;
break;
default:
exit('请输入正确指令!');
}
}
public function mode (int $mode = self::MODE_NORMAL) :void
{
$this->mode = $mode;
switch ($mode) {
case 0:
echo '切换正常模式' . PHP_EOL;
break;
case 1:
echo '切换摇头模式' . PHP_EOL;
break;
default:
exit('请输入正确指令!');
}
}
}
命令类
接下来,我们来定义command接口
namespace app\command;
interface Command
{
function execute ():void;
}
然后,来完成风扇的速度调节的具体命令类
class FanSpeedCommand implements Command
{
private $receiver = null;
private $speed = 0;
public function __construct(FanReceiver $receiver, $speed = 0)
{
$this->speed = $speed;
}
public function execute(): void
{
$this->receiver->speed($this->speed);
}
}
还有模式调整的具体命令类
class FanModeCommand implements Command
{
private $receiver = null;
private $mode = 0;
public function __construct(FanReceiver $receiver, $mode = 0)
{
$this->receiver = $receiver;
$this->mode = $mode;
}
public function execute(): void
{
$this->receiver->mode($this->mode);
}
}
调用者
class Invoke
{
private $command = null;
public function setCommand (Command $command) :void
{
$this->command = $command;
}
public function call () :void
{
$this->command->execute();
}
}
测试
echo '天热,想吹风扇' . PHP_EOL;
$receiver = new FanReceiver();
$command = new FanSpeedCommand($receiver, 1);
$invoke = new Invoke();
$invoke->setCommand($command);
$invoke->call();
echo '还不够爽,调最大的风' . PHP_EOL;
$command = new FanSpeedCommand($receiver, 3);
$invoke->setCommand($command);
$invoke->call();
echo '要睡觉了,调睡眠状态' . PHP_EOL;
$command = new FanModeCommand($receiver, 1);
$invoke->setCommand($command);
$invoke->call();
echo '出门了,关掉风扇' . PHP_EOL;
$command = new FanSpeedCommand($receiver, 0);
$invoke->setCommand($command);
$invoke->call();
具体的输出结果为:
天热,想吹风扇
切换低速风
还不够爽,调最大的风
切换高速风
要睡觉了,调睡眠状态
切换睡眠模式
出门了,关掉风扇
关闭风扇
撤销功能
使用命令行模式,是非常容易实现undo(撤销)功能。现在,我们来看看如何实现撤销的功能。首先,修改命令接口,加入undo方法,让每个具体命令对象都去实现undo方法。
interface Command
{
function execute ():void;
function undo () :void;
}
接下来,修改调用者对象,新增一个实现撤销的方法。
public function revocation () :void
{
$this->command->undo();
}
再然后,我们就需要思考如何去完成这个功能了。其实也非常的简单,我们只要修改接受者,能够获取风速以及模式的属性就好。然后,在具体的命令对象中,每次执行action前,保存该属性即可。拿到这个属性,然后就能做到undo操作了。
修改接受者即FanReceiver,新增魔术方法,可以获取属性
public function __get ($arg) {
return $this->$arg ?? null;
}
修改具体命令类FanSpeedCommand,新增属性$prevSpeed用来保存上一次的状态
private $prevSpeed = 0;
然后修改execute方法新增undo方法,代码如下:
public function execute(): void
{
$this->prevSpeed = $this->receiver->speed;
$this->receiver->speed($this->speed);
}
public function undo(): void
{
$this->receiver->speed($this->prevSpeed);
}
FanModeCommand类的修改与上面几乎一致,所以不赘述了。
接下来思考一个问题:现在我们的确是实现了undo操作,但是只是一层的,如何去实现多层的undo呢?其实也很简单,我们只需要弄一个堆栈记录每次的操作即可。
宏命令
正常情况下,当我们在遥控器中按下睡眠模式时,不关是模式变为了,风速也会变为最小的风。现在,我们需要去完成一个宏命令。命令行模式非常容易完成宏命令。
新建一个宏命令对象:
class MacroCommand implements Command
{
private $commands = [];
public function __construct($commands = [])
{
$this->commands = $commands;
}
public function execute(): void
{
foreach ($this->commands as $command) {
$command->execute();
}
}
public function undo(): void
{
foreach ($this->commands as $command) {
$command->undo();
}
}
}
测试代码如下:
$receiver = new FanReceiver();
$command = new FanSpeedCommand($receiver, 1);
$command2 = new FanModeCommand($receiver, 1);
$macroCommand = new MacroCommand([$command, $command2]);
$invoke = new Invoke();
$invoke->setCommand($macroCommand);
$invoke->call();
文章部分内容参考自 https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/command.html