用遥控器的例子逐步介绍命令模式
摘要
这一篇文章用一个非常非常详细的用遥控器控制家电的例子来解释命令模式,用命令模式和不用命令模式的情况都有了具体的代码,方便大家做出比较,进而得到命令模式的优缺点。一开始我不会直接给出命令模式的定义,在例子中适合的时候我才会给出定义,方便读者循序渐进地理解命令模式的设计动机。
这篇文章写了很久,各位看官走过路过不要错过,都进来看一眼吧。
模式动机
在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。
例子
我想了好久用什么好例子来介绍命令模式,最后发现还是《Head First设计模式》中的例子最恰当。
下面是例子介绍:
你所在的公司从家电公司处接到了一个任务,家电公司给你提供各种家具如风扇、窗帘、电灯等的具体实现类,你需要设计一个遥控器类来控制提供的家具,前提是你不能修改提供的家电类的代码。
注意,不同的家电类中的方法接口不一定一样,比如开风扇是fan.open() 而开电灯则是light.on()。
家电公司给的第一个任务是实现一个控制风扇窗帘和电灯开关的遥控器。
我们先看一下家电公司提供的家电类的代码
Furniture.java
package priv.mxz.design_pattern.command_pattern;
class Curtain {
public void turnon(){
System.out.println("[Curtain]:turnon");
}
public void turnoff(){
System.out.println("[Curtain]:turnoff");
}
}
class Fan {
public void open(){
System.out.println("[Fan]:open");
}
public void close(){
System.out.println("[Fan]:close");
}
}
class Light {
public void on(){
System.out.println("[Light]:on");
}
public void off(){
System.out.println("[Light]:off");
}
}
可以看出不同家电类中开关操作的接口都是不一样的。
不使用命令模式的实现
实现一个控制风扇窗帘和电灯开关的遥控器这个任务落在了你公司的实习生阿呆手上,下面是阿呆的实现
SimpleController.java
package priv.mxz.design_pattern.command_pattern;
enum ControllerButton {
CURTAINON,CURTAINOFF,FANON,FANOFF,LIGHTON,LIGHTOFF;
}
class SimpleController {
private Curtain curtain=null;
private Fan fan=null;
private Light light=null;
public SimpleController(Curtain curtain, Fan fan, Light light){
this.curtain=curtain;
this.fan=fan;
this.light=light;
}
public void pushButton(ControllerButton button){
switch (button){
case CURTAINON:
curtain.turnon();
break;
case CURTAINOFF:
curtain.turnoff();
break;
case FANON:
fan.open();
break;
case FANOFF:
fan.close();
break;
case LIGHTON:
light.on();
break;
case LIGHTOFF:
light.off();
break;
default:
break;
}
}
}
class SimpleControllerTest{
public static void main(String[] args) {
SimpleController simpleController=new SimpleController(new Curtain(),new Fan(),new Light());
for (ControllerButton button:ControllerButton.values()){
simpleController.pushButton(button);
}
}
}
SImpleControllerTest中有程序入口,对SimpleController进行测试,测试后发现这个遥控器的基本功能已经得到实现。
下面是输出结果
[Curtain]:turnon
[Curtain]:turnoff
[Fan]:open
[Fan]:close
[Light]:on
[Light]:off
把这个遥控器类交给家电公司后,家电公司觉得很“不错”,于是把其他遥控器的实现也交给了你所在的公司,这些控制器包括:只控制风扇和电灯的遥控器、控制门和空调的遥控器、控制风扇和空调和热水器的遥控器等等等等。。。
这些遥控器再次交给了实习生阿呆来实现,在用同样的方式实现了几个遥控器后,阿呆陷入了思考,因为他发现不同的遥控器的实现是非常类似的,都是用switch case来判断按键对应的家电类与方法,有没有办法可以把类似的行为抽象出来呢?比如定义一个父类?阿呆思考着能不能够把最重要的方法pushButton放到父类中?结果发现是不可以的,pushButton里面需要执行绑定的家电类实例的方法,而这些家电类实例是每一个子类都不一样的,无法统一放到父类中。
使用命令模式的实现
阿呆把他的这个困惑告诉了他的导师阿强,阿强看完阿呆的代码后,说出了无法抽象遥控器行为的原因:遥控器类直接与家电类进行交互了,遥控器类与家电类之间存在强耦合,所以无法很好地把遥控器的行为从家电类中分离出来。要想把遥控器的行为抽象出来,必须让遥控器与家电类实现解耦。
在解释完后,阿强又给阿呆介绍了命令模式,然后用命令模式做了基础遥控器类的实现。
命令模式的定义
将请求封装成对象,这可以让你使用不同的请求、队列或者日志请求来参数化其他对象,命令模式可以支持撤销操作。
模式结构
命令模式包含以下角色:
- Command 抽象命令类
- ConcreteCommand 具体命令类
- Invoker 调用者
- Receiver 接收者
- Client 客户类
下面的UML类图介绍了各个角色之间的关系
从上往下看,Client类表示用户,用户通过向Invoker传入具体的Command以及Command的Receiver(在new Command时传入),来指定用户需要执行的指令以及指令接收者,用户不需要知道指令接收者详细要调用什么方法来完成指令要求的行为,只需要把要指令接收者要做的事情通过命令来传达。
Invoker类表示调用者,负责与Command交互,Invoker并不清楚用户传给自己的Command具体是负责什么的,它只负责在适当的时候调用Command的execute方法。
Command是抽象接口,定义execute方法来执行自身的命令。
ConcreteCommandA(B)是实现Command接口的具体类,内部有该command的接收者receiver的实例,execute方法的具体实现就是调用receiver的某些方法来实现命令表示的功能。
ReceiverA(B)命令的接收者类,注意,不同的receiver虽然角色类似,但不一定都继承于相同的类或者有相同的接口,不同的receiver可以是完全无关系的类,具体怎么使用这些类的方法就交给对应的ConcreteCommand负责。
具体代码实现
命令模式中的对象与例子中的类对应关系如下
命令模式中 | 例子中 |
---|---|
Receiver | Fan Light Curtain |
Command | Command |
ConcreteCommand | FanOnCommand FanOffCommand LightOnCommand 。。。 |
Invoker | CommandController |
Client | CommandControllerTest |
下面是代码
Command.java
package priv.mxz.design_pattern.command_pattern;
interface Command {
void execute();
}
class CurtainOnCommand implements Command{
private Curtain curtain;
public CurtainOnCommand(Curtain curtain){
this.curtain=curtain;
}
@Override
public void execute() {
curtain.turnon();
}
}
class CurtainOffCommand implements Command{
private Curtain curtain;
public CurtainOffCommand(Curtain curtain){
this.curtain=curtain;
}
@Override
public void execute() {
curtain.turnon();
}
}
class FanOnCommand implements Command{
private Fan fan;
public FanOnCommand(Fan fan){
this.fan=fan;
}
@Override
public void execute() {
fan.open();
}
}
class FanOffCommand implements Command{
private Fan fan;
public FanOffCommand(Fan fan){
this.fan=fan;
}
@Override
public void execute() {
fan.close();
}
}
class LightOnCommand implements Command{
private Light light;
public LightOnCommand(Light light){
this.light=light;
}
@Override
public void execute() {
light.on();
}
}
class LightOffCommand implements Command{
private Light light;
public LightOffCommand(Light light){
this.light=light;
}
@Override
public void execute() {
light.off();
}
}
class NoCommand implements Command{
@Override
public void execute() {
// do nothing
}
}
Command.java定义了Command接口和其他具体的Command类,每一个对家电的操作对应一个具体的Command类,比如CurtainOnCommand对应拉开窗帘的操作。
特别的一个实现Command接口的类是NoCommand,这是一个空对象,用于遥控器中对命令的初始化设置。
CommandController.java
package priv.mxz.design_pattern.command_pattern;
import java.util.ArrayList;
public class CommandController {
ArrayList<Command> commands;
public CommandController(int buttonSize){
if (buttonSize>0){
commands=new ArrayList<Command>(buttonSize);
for(int i=0; i<buttonSize; i++){
commands.add(i,new NoCommand());
}
}
}
public void setCommand(int buttonIndex, Command command){
if (buttonIndex>=0 && buttonIndex<commands.size()){
commands.set(buttonIndex,command);
}
}
public void pushButton(int buttonIndex){
if (buttonIndex>=0&& buttonIndex<commands.size()) {
commands.get(buttonIndex).execute();
}
}
}
CommandController定义了一个用命令模式实现的遥控器,在内部存储Command数组,每一个Command实例对应一个按钮,在构造函数中初始化Command数组大小(即按钮的数目),然后把每个指令都设置为NoCommand实例,这么做的原因是解决下述的一种情况:用户没有在对应按钮上调用setCommand绑定新的指令,就调用pushButton来执行还没绑定的指令(即执行command.execute())。 这种情况下如果Command数组中对应的command没有被初始化,就会报异常。NoCommand类里面execute方法什么都不干,即使用户调用也不会抛异常和对系统造成影响。
CommandControllerTest.java
package priv.mxz.design_pattern.command_pattern;
public class CommandControllerTest {
public static void main(String[] args) {
CommandController commandController=new CommandController(6);
Curtain curtain=new Curtain();
commandController.setCommand(0,new CurtainOnCommand(curtain));
commandController.setCommand(1,new CurtainOffCommand(curtain));
Fan fan=new Fan();
commandController.setCommand(2,new FanOnCommand(fan));
commandController.setCommand(3,new FanOffCommand(fan));
Light light=new Light();
commandController.setCommand(4,new LightOnCommand(light));
commandController.setCommand(5,new LightOffCommand(light));
for(int i=0; i<6; i++){
commandController.pushButton(i);
}
}
}
CommandControllerTest中从客户的角度定义了一个遥控器,这个遥控器有六个按钮,功能按顺序是开窗帘、关窗帘、开风扇、关风扇、开灯和关灯。我们通过setCommand配置好按钮对应的command,最后依次按下按钮,下面是输出
[Curtain]:turnon
[Curtain]:turnon
[Fan]:open
[Fan]:close
[Light]:on
[Light]:off
例子小结
我们对比一下不使用命令模式的遥控器实现和使用了命令模式的遥控器实现。
- 不使用命令模式的情况下,SimpleController类需要与多个家电类进行交互,在SimpleController类内部需要存储具体的家电类实例,在按下按钮判断执行什么功能时需要使用大量的if判断或者switch。如果要控制新的家电或者修改按键对应的功能必须修改控制器内部的代码,不符合开闭原则。SimpleController是遥控器的一个具体实现,只能对应一种具体的遥控器。(如果你问为什么不抽象出一个遥控器基类出来,我只能告诉你,你可以去试试,然后会发现无法抽象出很有用的功能出来,最多只能定义一下接口)
- 使用命令模式的情况下,我们实现了一个CommandController类,它不需要与各种各样的家电进行交互,而是选择与具有统一Command接口的类进行交互,在类内部不需要存储每个家电类的实例,只需要存储command数组即可,抽象的command接口可以使CommandController类实例具有更丰富的变化,这符合“依赖倒置”的设计原则(依赖抽象类,不要依赖具体类)。我们可以基于这个类实现有不同数目按钮和控制不同家电的遥控器,这个类本身可以多次复用。需要控制新的家电只需要定义好对应的Command类,然后再使用CommandController类实例时传入command就好,CommandController类的代码无需改动,就可以得到一个控制新家电的一个遥控器。我们要改变遥控器上某个按钮的功能也十分方便,只需要用setCommand重新传入需要的command就可以。
命令模式的扩展
继续使用上述遥控器的例子,如果我们需要支持一个按键(一个command)就可以实现多个动作,即宏命令,我们可以定义一个实现了Command接口且自身存储多个command的类,下面是一种实现
MacroCommand.java
class MacroCommand implements Command{
private ArrayList<Command> commands;
public MacroCommand(ArrayList<Command> commands){
this.commands=commands;
}
@Override
public void execute() {
if (commands!=null){
for(Command command:commands){
command.execute();
}
}
}
}
我们也可以用命令模式支持遥控器的撤回操作(undo)具体只需在Command接口新加一个undo方法,然后在对应的类中实现具体的行为,比如LightOnCommand的undo方法只需要调用light.off()
使用场景
命令模式适合用于
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起,即支持宏命令
优缺点
命令模式的优点
- 降低系统的耦合度。
- 新的命令可以很容易地加入到系统中。
- 可以比较容易地设计一个命令队列和宏命令(组合命令)。
- 可以方便地实现对请求的Undo和Redo。
命令模式的缺点
- 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
总结
- 在命令模式中,将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作模式或事务模式。
- 命令模式包含四个角色:抽象命令类中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作;具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中;调用者即请求的发送者,又称为请求者,它通过命令对象来执行请求;接收者执行与请求相关的操作,它具体实现对请求的业务处理。
- 命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
- 命令模式的主要优点在于降低系统的耦合度,增加新的命令很方便,而且可以比较容易地设计一个命令队列和宏命令,并方便地实现对请求的撤销和恢复;其主要缺点在于可能会导致某些系统有过多的具体命令类。
- 命令模式适用情况包括:需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互;需要在不同的时间指定请求、将请求排队和执行请求;需要支持命令的撤销操作和恢复操作,需要将一组操作组合在一起,即支持宏命令
参考
https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/command.html