Head First 设计模式之第六章——命令模式

概念

在讲命令模式之前,我们先假设如下的一个场景:假设有一个万能遥控,它可以控制多种家电的开关,如电灯、电视、电饭煲等等。如果要实现这样的一个万能遥控,那最直接的方法就是创建一个RemoteControl的类,在这个类中,实现创建电灯、电视等的对象,然后调用它们的on与off的方法来控制这些电器的开关。但这样做有一个很不好的后果,就是,如果我想用这个万能遥控来控制空调(谁叫你是万能遥控),那我岂不是要修改RemoteControl类,并在其中创建空调对象,然后直接调用空调的on与off方法。这对应于现实生活中就是:如果想要控制一个新的电器,那我就要主动去升级这个万能遥控的系统,这显然是十分的笨拙的,因为这明显违反了开发-封闭原则。

这时就要想到面向对象中的一个设计原则:对可能产生变化的部分进行封装。在这个场景中,万能遥控的开发功能是固定不变的,而最容易变化的是电器的种类,毕竟每个人家里购置新的电器也很常见嘛。所以要对这个变化进行封装。那如何封装呢?此时命令模式就应该派上用场了。

下图中的类图就是命令模式的类图,命令模式的定义为:**将“请求”封装成对象,以便使用不同的请、队列或者日志来参数化其他对象,命令模式也支持可撤销的操作。**这个定义初次看到时,真是令人一脸懵逼,不过不要紧,世上无难事,只要肯放弃。所以暂时先不用理会这个定义,忽略就好。我们先按下方的类图直接讲讲命令模式如何用就可以了。

命令模式最主要的作用是将具体的调用者与具体的接收者进行解耦。在命令模式中主要有三个角色:

  1. 接收者(Receive),这个角色就是最终干活的那个对象,在上面的场景中,就是那些电器,所有动作最终都是由这些具体的电器来执行的;
  2. 命令(Command),这个角色主要是用来声明并定义具体的命令,即定义如何执行接收者的具体操作。在上述的例子中,相当于定义一个类,来控制具体的电视是如何开与如何关,并在这个类中定义一个execute()方法,一个调用这个方法,电视的开与关操作就可以被执行;
  3. 调用者(Invoker),它主要是接收命令,并调用命令,如上述场景中的万能遥控,可以将控制电视、控制电灯等命令加载到遥控中,并通过按键来执行这些命令。

我们还是看回命令模式的类图,ICommand接口类型的对象是Invoker中的组成部分,而ConcreteCommand是实现了ICommand接口的具体命令类,主要是实现了execute()方法,而在这个方法中,则使用了具体的Receiver的具体action()。如果有很多个Receiver,有多个不同的ConcreteCommand类来执行不同的Receiver的操作,使用时只需要将这些ConcreteCommand对象传入到Invoker中的ICommand类对象即可,在Invoker中,只需要调用ICommand类对象的execute()方法即可执行具体的命令(此处使用了多态技术)。Invoker永远只依赖ICommand接口类的对象,而不会依赖具体的命令类,这体现了面向对象中的“要依赖接口,而不是依赖具体实现”的指导思想。上述的做法就很好地将Invoker与Receiver进行解耦,Invoker不需要知道具体的Receiver,它只要无脑地调用对应的ICommand接口类对象即可。一旦需要为遥控添加新的电器,那只需要添加具体的Receiver类与具体的命令类即可,这也符合开放-封闭原则:对扩展开发,对修改封闭。
Head First 设计模式之第六章——命令模式

应用

用程序来模拟一下餐厅的点餐过程:客户来到餐厅,通过服务员下了一个菜单,服务员根据菜单通知各种厨师去做相应的菜品,最终厨师做出菜品并送到客户的桌上。根据命令模式,可以设计出如下类图:
Head First 设计模式之第六章——命令模式

IOrder相当于命令模式中ICommand,它代表一个菜单接口,然后通过实现这个接口可以定义各种各样的菜单,如茶的菜单,咖啡的菜单,中餐的菜单,西餐的菜单,上述的菜单对应茶的厨师、咖啡的厨师、中餐厨师、西餐厨师,这些厨师相当于命令模式中的Receiver,而服务员WaiterInvoker相当于命令模式中的Invoker,顾客通过服务员来点餐。服务员通过菜单来通知厨师做菜,具体的菜单会通知对应的厨师生产菜品,服务员与厨师之间是完全解耦时,他们之间不需要相互认识,他们之间的沟通桥梁就是命令(即类图中的IOrder)。

具体的代码稍后上传。