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