装修新房的最后几道工序之一是安装插座和开关,经过开关能够控制一些电器的打开和关闭,例如电灯或者排气扇。在购买开关时,咱们并不知道它未来到底用于控制什么电器,也就是说,开关与电灯、排气扇并没有直接关系,一个开关在安装以后可能用来控制电灯,也可能用来控制排气扇或者其余电器设备。开关与电器之间经过电线创建链接,若是开关打开,则电线通电,电器工做;反之,开关关闭,电线断电,电器中止工做。相同的开关能够经过不一样的电线来控制不一样的电器。php
在软件开发中也存在不少与开关和电器相似的请求发送者和接收者对象,例如一个按钮,它多是一个“关闭窗口”请求的发送者,而按钮点击事件处理类则是该请求的接收者。为了下降系统的耦合度,将请求的发送者和接收者解耦,咱们可使用一种被称之为命令模式的设计模式来设计系统,在命令模式中,发送者与接收者之间引入了新的命令对象,将发送者的请求封装在命令对象中,再经过命令对象来调用接收者的方法。编程
在软件开发中,咱们常常须要向某些对象发送请求(调用其中的某个或某些方法),可是并不知道请求的接收者是谁,也不知道被请求的操做是哪一个,此时,咱们特别但愿可以以一种松耦合的方式来设计软件,使得请求发送者与请求接收者可以消除彼此之间的耦合,让对象之间的调用关系更加灵活,能够灵活地指定请求接收者以及被请求的操做。命令模式为此类问题提供了一个较为完美的解决方案。设计模式
命令模式能够将请求发送者和接收者彻底解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只须要知道如何发送请求,而没必要知道如何完成请求。数组
命令模式定义以下:多线程
命令模式(Command Pattern):将一个请求封装为一个对象,从而让咱们可用不一样的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操做。命令模式是一种对象行为型模式,其别名为动做(Action)模式或事务(Transaction)模式。
命令模式的定义比较复杂,提到了不少术语,例如“用不一样的请求对客户进行参数化”、“对请求排队”,“记录请求日志”、“支持可撤销操做”等,在后面将对这些术语进行一一讲解。并发
命令模式的核心在于引入了命令类,经过命令类来下降发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再经过命令对象来调用请求接收者的处理方法,其结构如图所示:工具
在命令模式结构图中包含以下几个角色:学习
命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。每个命令都是一个操做:请求的一方发出请求要求执行一个操做;接收的一方收到请求,并执行相应的操做。命令模式容许请求的一方和接收的一方独立开来,使得请求的一方没必要知道接收请求的一方的接口,更没必要知道请求如何被接收、操做是否被执行、什么时候被执行,以及是怎么被执行的。this
命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。在最简单的抽象命令类中只包含了一个抽象的execute()方法,每一个具体命令类将一个Receiver类型的对象做为一个实例变量进行存储,从而具体指定一个请求的接收者,不一样的具体命令类提供了execute()方法的不一样实现,并调用不一样接收者的请求处理方法。spa
典型的抽象命令类代码以下所示:
abstract class Command { abstract public function execute(); }
对于请求发送者即调用者而言,将针对抽象命令类进行编程,能够经过构造注入或者设值注入的方式在运行时传入具体命令类对象,并在业务方法中调用命令对象的execute()方法,其典型代码以下所示:
class Invoker { private $command; //构造注入 public function __construct(Command $command) { $this->command = $command; } //设值注入 public function setCommand(Command $command) { $this->command = $command; } //业务方法,用于调用命令类的execute()方法 public function call() { $this->command->execute(); } }
具体命令类继承了抽象命令类,它与请求接收者相关联,实现了在抽象命令类中声明的execute()方法,并在实现时调用接收者的请求响应方法action(),其典型代码以下所示:
class ConcreteCommand extends Command { //维持一个对请求接收者对象的引用 private $receiver; public function execute() { //调用请求接收者的业务处理方法action() $this->receiver->action(); } }
请求接收者Receiver类具体实现对请求的业务处理,它提供了action()方法,用于执行与请求相关的操做,其典型代码以下所示:
class Receiver { public function action() { //具体操做 } }
Sunny软件公司开发人员为公司内部OA系统开发了一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户能够经过这些功能键来实现一些快捷操做。Sunny软件公司开发人员经过分析,发现不一样的用户可能会有不一样的使用习惯,在设置功能键的时候每一个人都有本身的喜爱,例若有的人喜欢将第一个功能键设置为“打开帮助文档”,有的人则喜欢将该功能键设置为“最小化至托盘”,为了让用户可以灵活地进行功能键的设置,开发人员提供了一个“功能键设置”窗口,该窗口界面如图所示:
经过如图所示界面,用户能够将功能键和相应功能绑定在一块儿,还能够根据须要来修改功能键的设置,并且系统在将来可能还会增长一些新的功能或功能键。
为了下降功能键与功能处理类之间的耦合度,让用户能够自定义每个功能键的功能,Sunny软件公司开发人员使用命令模式来设计“自定义功能键”模块,其核心结构如图所示:
FBSettingWindow是“功能键设置”界面类,FunctionButton充当请求调用者,Command充当抽象命令类,MinimizeCommand和HelpCommand充当具体命令类,WindowHanlder和HelpHandler充当请求接收者。完整代码以下所示:
<?php //功能键设置窗口类 class FBSettingWindow { private $title; //窗口标题 //定义一个数组来存储全部功能键 private $functionButtons = []; public function __construct(string $title) { $this->title = $title; } public function setTitle(string $title) { $this->title = $title; } public function getTitle(): string { return $this->title; } public function addFunctionButton(FunctionButton $fb) { $this->functionButtons[] = $fb; } public function removeFunctionButton(FunctionButton $fb) { unset($this->functionButtons[array_search($fb, $this->functionButtons)]); } //显示窗口及功能键 public function display() { foreach ($this->functionButtons as $button) { $button->getName(); } } } //功能键类:请求发送者 class FunctionButton { private $name; //功能键名称 private $command; //维持一个抽象命令对象的引用 public function __construct(string $name) { $this->name = $name; } public function getName(): string { return $this->name; } //为功能键注入命令 public function setCommand(Command $command) { $this->command = $command; } //发送请求的方法 public function onClick() { $this->command->execute(); } } //抽象命令类 abstract class Command { abstract public function execute(); } //帮助命令类:具体命令类 class HelpCommand extends Command { private $hhObj; //维持对请求接收者的引用 public function __construct() { $this->hhObj = new HelpHandler(); } //命令执行方法,将调用请求接收者的业务方法 public function execute() { $this->hhObj->display(); } } //最小化命令类:具体命令类 class MinimizeCommand extends Command { private $whObj; //维持对请求接收者的引用 public function __construct() { $this->whObj = new WindowHanlder(); } //命令执行方法,将调用请求接收者的业务方法 public function execute() { $this->whObj->minimize(); } } //窗口处理类:请求接收者 class WindowHanlder { public function minimize() { echo '窗口最小化'; } } //帮助文档处理类:请求接收者 class HelpHandler { public function display() { echo '显示帮助文档'; } }
为了提升系统的灵活性和可扩展性,咱们将具体命令类的类名存储在配置文件中,并经过工具类ConfigUtil来读取配置文件并反射生成对象。
若是须要修改功能键的功能,例如某个功能键能够实现“自动截屏”,只须要对应增长一个新的具体命令类,在该命令类与屏幕处理者(ScreenHandler)之间建立一个关联关系,而后将该具体命令类的对象经过配置文件注入到某个功能键便可,原有代码无须修改,符合“开闭原则”。在此过程当中,每个具体命令类对应一个请求的处理者(接收者),经过向请求发送者注入不一样的具体命令对象可使得相同的发送者对应不一样的接收者,从而实现“将一个请求封装为一个对象,用不一样的请求对客户进行参数化”,客户端只须要将具体命令对象做为参数注入请求发送者,无须直接操做请求的接收者。
有时候咱们须要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,咱们能够经过命令队列来实现。
命令队列的实现方法有多种形式,其中最经常使用、灵活性最好的一种方式是增长一个CommandQueue类,由该类来负责存储多个命令对象,而不一样的命令对象能够对应不一样的请求接收者,CommandQueue类的典型代码以下所示:
class CommandQueue { //定义一个数组来存储命令队列 private $commands = []; public function addCommand(Command $command) { $this->commands[] = $command; } public function removeCommand(Command $command) { unset($this->commands[array_search($command, $this->commands)]); } //循环调用每个命令对象的execute()方法 public function execute() { foreach ($this->commands as $command) { $command->execute(); } } }
在增长了命令队列类CommandQueue之后,请求发送者类Invoker将针对CommandQueue编程,代码修改以下:
class Invoker { private $commandQueue; //维持一个CommandQueue对象的引用 //构造注入 public function __construct(CommandQueue $commandQueue) { $this->commandQueue = $commandQueue; } //设值注入 public function setCommandQueue(CommandQueue $commandQueue) { $this->commandQueue = $commandQueue; } //调用CommandQueue类的execute()方法 public function call() { $this->commandQueue->execute(); } }
命令队列与咱们常说的“批处理”有点相似。批处理,顾名思义,能够对一组对象(命令)进行批量处理,当一个发送者发送请求后,将有一系列接收者对请求做出响应,命令队列能够用于设计批处理应用程序,若是请求接收者的接收次序没有严格的前后次序,咱们还可使用多线程技术来并发调用命令对象的execute()方法,从而提升程序的执行效率。
在命令模式中,咱们能够经过调用一个命令对象的execute()方法来实现对请求的处理,若是须要撤销(Undo)请求,可经过在命令类中增长一个逆向操做来实现。
下面经过一个简单的实例来学习如何使用命令模式实现撤销操做:
Sunny软件公司欲开发一个简易计算器,该计算器能够实现简单的数学运算,还能够对运算实施撤销操做。
Sunny软件公司开发人员使用命令模式设计了如图所示结构图,其中计算器界面类CalculatorForm充当请求发送者,实现了数据求和功能的加法类Adder充当请求接收者,界面类可间接调用加法类中的add()方法实现加法运算,而且提供了可撤销加法运算的undo()方法。
本实例完整代码以下所示:
<?php //加法类:请求接收者 class Adder { private $num = 0; //定义初始值为0 //加法操做,每次将传入的值与num做加法运算,再将结果返回 public function add(int $value): int { return $this->num += $value; } } //抽象命令类 abstract class AbstractCommand { abstract public function execute(int $value): int; //声明命令执行方法execute() abstract public function undo(): int; //声明撤销方法undo() } //具体命令类 class ConcreteCommand extends AbstractCommand { private $adder; private $value; public function __construct() { $this->adder = new Adder(); } //实现抽象命令类中声明的execute()方法,调用加法类的加法操做 public function execute(int $value): int { $this->value = $value; return $this->adder->add($value); } //实现抽象命令类中声明的undo()方法,经过加一个相反数来实现加法的逆向操做 public function undo(): int { return $this->adder->add(-$this->value); } } //计算器界面类:请求发送者 class CalculatorForm { private $command; public function setCommand(AbstractCommand $command) { $this->command = $command; } //调用命令对象的execute()方法执行运算 public function compute(int $value) { return $this->command->execute($value); } //调用命令对象的undo()方法执行撤销 public function undo() { return $this->command->undo(); } }
须要注意的是在本实例中只能实现一步撤销操做,由于没有保存命令对象的历史状态,能够经过引入一个命令集合或其余方式来存储每一次操做时命令的状态,从而实现屡次撤销操做。除了Undo操做外,还能够采用相似的方式实现恢复(Redo)操做,即恢复所撤销的操做(或称为二次撤销)。
请求日志就是将请求的历史记录保存下来,一般以日志文件(Log File)的形式永久存储在计算机中。不少系统都提供了日志文件,例如Windows日志文件、Oracle日志文件等,日志文件能够记录用户对系统的一些操做(例如对数据的更改)。请求日志文件能够实现不少功能,经常使用功能以下:
在实现请求日志时,咱们能够将命令对象经过序列化写到日志文件中。
宏命令(Macro Command)又称为组合命令,它是组合模式和命令模式联用的产物。宏命令是一个具体命令类,它拥有一个集合属性,在该集合中包含了对其余命令对象的引用。一般宏命令不直接与请求接收者交互,而是经过它的成员来调用接收者的方法。当调用宏命令的execute()方法时,将递归调用它所包含的每一个成员命令的execute()方法,一个宏命令的成员能够是简单命令,还能够继续是宏命令。执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理,其结构如图所示:
命令模式是一种使用频率很是高的设计模式,它能够将请求发送者与接收者解耦,请求发送者经过命令对象来间接引用请求接收者,使得系统具备更好的灵活性和可扩展性。在基于GUI的软件开发,不管是在电脑桌面应用仍是在移动应用中,命令模式都获得了普遍的应用。
使用命令模式可能会致使某些系统有过多的具体命令类。由于针对每个对请求接收者的调用操做都须要设计一个具体命令类,所以在某些系统中可能须要提供大量的具体命令类,这将影响命令模式的使用。