Design Pattern学习笔记之命令(Command Pattern)

Design Pattern学习笔记之命令(Command Pattern)

1.    引子--Whois?

a.      用于封装方法调用。

b.      可以很方便地提供“undo”操作。

c.      可以用于进行系统恢复。

d.      实现了方法调用者和方法具体实现者之间的松散耦合。

e.      相对于其他模式而言,比较难理解。

2.    问题引入—远程控制

鉴于你在气象观测站设计工作中的优秀表现,有顾客找上你设计一个对家庭电器进行远程控制的接口,用于远程控制家里的电视机、车库门、室外灯、花园灯…

创意不错,先看看我们掌握什么:

1.      各个设备的厂家提供了控制该设备的接口,调用接口就能控制设备。

2.      顾客提供物理的模拟器负责装载你提供的远程控制程序,将命令发送到各个设备。

看看我们的物理设备:

该设备提供7个编程接口,每个接口用于远程控制一种设备,每个接口对应一组“on”、“off”按钮,用于控制设备的开关;此外还有一个undo按钮,用于撤销前一个操作。

看看不同设备的控制接口:

重申下我们要做的工作:

1.      为远程控制器设计接口

2.      该接口满足控制现有多种设备的要求

3.      该接口满足新增设备时对已有代码不造成影响

3.    思考练习—howto implement?

根据需求得出目标:

a.  远程控制器应该提供on、off、以及undo至少三个方法

b.  看起来远程控制器应该了解按钮的功能,但是不应该了解具体怎么实现on、off、undo

模式来了:

1.      command模式能实现请求者和实现者之间的松散耦合,应用到远程控制器的例子中,远程控制器正好可以作为请求者,各种厂家提供的家电产品正好可以作为实现者。

2.      怎么实现?引入“command”类,用于封装一次具体的请求和一个具体的电器(负责具体请求的实现),这样我们为远程控制器的控制按钮设置“command”,当按钮按下时,远程控制器只是调用command做工作,实现了远程控制器跟具体电器的松散耦合。

3.      由于远程控制器只了解command,因此可以为command设置统一的接口,不需要了解具体的电器,远程控制器就变得相当简单和稳定。

4.    类比—命令模式与餐厅点餐

命令模式看起来不太好理解,我们使用大家比较熟悉的餐馆点餐的流程来类比一下:

顾客按照菜单将要点的菜记下来,服务员跟客户确认订单,将订单交给厨师,厨师按照顺序处理订单。在整个过程中,服务员无需了解具体菜品的制作过程,只需知道顾客点的是什么菜,然后把订单交给厨师即可。

         如果从面向对象的角度考虑问题,我们抽取出各个类,实际场景中的交互通过类之间调用方法实现,图示变成以下内容:

顾客类调用订单类的createOrder方法生成订单,服务员类调用takeOrder方法获取订单,服务员类确认订单后,调用订单类的orderUp方法提交订单,订单调用厨师的makeBurger方法和makeShake方法,完成菜品的制作。

         来看看顾客点餐过程中类的角色:

1.      订单类记录顾客点的菜品,在整个过程中被顾客传递给服务员,又从服务员传递给厨师。

2.      各种顾客的订单千差万别,但是所有的订单都拥有一个提交订单的方法orderUp,服务员对所有的订单都是调用orderUp方法,就能完成提交订单的工作。

3.      订单持有一个厨师对象,由它实际完成订单中菜品的制作。

4.      服务员类很简单,只是从顾客那里接收订单,提交订单;服务员类不关注订单是怎么满足的,甚至不关注顾客点了什么。

5.      厨师类负责根据订单做好菜品,他掌握如何做菜的知识,但是跟服务员类松散耦合,不知道哪个服务员给的订单。

我们已经知道命令模式可以实现请求者与具体请求响应者之间的松散耦合,像餐馆点餐的例子中,下订单的服务员和厨师之间是松散耦合,应用到远程控制器的例子中,我们要是也有一个“订单”这样的类,就有可能实现在远程控制器上按下按钮(on、off)的部分代码和电器的控制代码的松散耦合(按下远程控制器的按钮后,只是简单调用orderUp方法)。

来看看命令模式中的角色,是不是跟餐馆点餐的例子很像?

Client负责创建command;

Command持有一个Receiver;

Client调用setCommand方法,将命令交给Invoker;

Invoker调用Command的execute方法;

Command调用自己持有的Receiver的方法完成工作

你能找出来餐馆订餐例子中的角色和命令模式中角色的对应关系么?

例子角色:顾客、服务员、厨师、orderUp方法、订单、takeOrder方法

模式角色:Comman,execute(),Client,Invoker,Receiver,setCommand()

5.    远程控制器—应用命令模式

我们先为所有的command设计一个统一的接口:

public interface Command {

        public void execute();

}

为开灯创建第一个具体的命令:

public class LightOnCommand implements Command {

        Light light

        publicLightOnCommand(Light light) {

           this.light = light;

        }

        public void execute() {

           light.on();

        }

}

我们的远程控制器:

public class SimpleRemoteControl {

    Command slot;

 

    public SimpleRemoteControl() {}

 

    public void setCommand(Command command) {

       slot = command;

    }

 

    public void buttonWasPressed() {

       slot.execute();

    }

}

测试一下:

public class RemoteControlTest {

    public static void main(String[] args) {

       SimpleRemoteControl remote = new SimpleRemoteControl();

       Light light = new Light();

       GarageDoor garageDoor = new GarageDoor();

       LightOnCommand lightOn = new LightOnCommand(light);

       GarageDoorOpenCommand garageOpen =

           newGarageDoorOpenCommand(garageDoor);

 

       remote.setCommand(lightOn);

       remote.buttonWasPressed();

       remote.setCommand(garageOpen);

       remote.buttonWasPressed();

    }

}

6.    理论篇—命令模式的定义

The CommandPattern encapsulates a request as an object, thereby letting you parameterizeother objects with different requests, queue or log requests, and supportundoable operations.

命令模式将请求封装成对象,从而实现应用不同请求参数化调用者,使用同一个调用者可以实现多种不同的请求,方便实现队列、日志以及系统恢复。

释义:

1.      封装。我们知道command对象封装了具体的请求和通过一组行为来实现请求的Receiver,command对外只暴露execute方法,其他类只知道调用execute方法来满足请求,不了解具体怎么实现请求。

2.      参数化对象。对于服务员而言,她一天可能会接很多顾客不同的订单,她无需了解每个具体的订单,只需知道每个订单都有orderUp方法;对于远程控制器而言,原来用于控制灯的按钮,也可以通过设置不同的command来控制车库,对远程控制器而言,所有的控制命令都是一样的,都有个execute方法。

3.      实现额外需求。可用于实现队列、日志以及实现撤销的功能,这个后续再说。

看看类图:

Client负责创建ConcreteCommand并为它设置一个Receiver

Invoker持有要执行的command,它决定什么时候执行command(调用execute方法)

Command为所有具体的command定义接口

ConcreteCommand实现请求和请求实现者(Receiver)之间的绑定,通过调用多个Receiver的action方法实现。

Receiver负责实现具体的请求。

7.    模式应用—远程控制器的实现

在远程控制器中,使用数组来记录on、off按钮对应的命令;提供set方法提供运行期间改变按钮行为的功能;按钮按下后,调用对应command的execute方法。

public class RemoteControl {

    Command[] onCommands;

    Command[] offCommands;

 

    public RemoteControl() {

       onCommands = new Command[7];

       offCommands = new Command[7];

 

       Command noCommand = new NoCommand();

       for (int i = 0; i < 7; i++) {

           onCommands[i] = noCommand;

           offCommands[i] = noCommand;

       }

    }

 

    public void setCommand(int slot, Command onCommand, Command offCommand) {

       onCommands[slot] = onCommand;

       offCommands[slot] = offCommand;

    }

 

    public void onButtonWasPushed(int slot) {

       onCommands[slot].execute();

    }

 

    public void offButtonWasPushed(int slot) {

       offCommands[slot].execute();

    }

 

    public String toString() {

       StringBuffer stringBuff = new StringBuffer();

       stringBuff.append("\n------Remote Control -------\n");

       for (int i = 0; i < onCommands.length; i++) {

           stringBuff.append("[slot" + i + "] " + onCommands[i].getClass().getName()

              + "    " + offCommands[i].getClass().getName() + "\n");

       }

       return stringBuff.toString();

    }

}

大家能注意到对两个保存具体command的数组,先进行了nocommand的初始化,为什么这样?

能避免客户端使用时,反复判断是否command已加载。

一个具体的Receiver实现:

public class Light {

    String location = "";

 

    public Light(String location) {

       this.location = location;

    }

 

    public void on() {

       System.out.println(location + " light ison");

    }

 

    public void off() {

       System.out.println(location + " light isoff");

    }

}

Light的一组command:

public class LightOffCommand implements Command {

    Light light;

 

    public LightOffCommand(Light light) {

       this.light = light;

    }

 

    public void execute() {

       light.off();

    }

}

public class LightOnCommand implements Command {

    Light light;

 

    public LightOnCommand(Light light) {

       this.light = light;

    }

 

    public void execute() {

       light.on();

    }

}

测试下:

public class RemoteLoader {

 

    public static void main(String[] args) {

       RemoteControl remoteControl = new RemoteControl();

 

       Light livingRoomLight = new Light("Living Room");

       Light kitchenLight = new Light("Kitchen");

       CeilingFan ceilingFan= new CeilingFan("Living Room");

       GarageDoor garageDoor = new GarageDoor("");

       Stereo stereo = new Stereo("LivingRoom");

 

       LightOnCommand livingRoomLightOn =

              new LightOnCommand(livingRoomLight);

       LightOffCommand livingRoomLightOff =

              new LightOffCommand(livingRoomLight);

       LightOnCommand kitchenLightOn =

              new LightOnCommand(kitchenLight);

       LightOffCommand kitchenLightOff =

              new LightOffCommand(kitchenLight);

 

       CeilingFanOnCommand ceilingFanOn =

              new CeilingFanOnCommand(ceilingFan);

       CeilingFanOffCommand ceilingFanOff =

              new CeilingFanOffCommand(ceilingFan);

 

       GarageDoorUpCommand garageDoorUp =

              new GarageDoorUpCommand(garageDoor);

       GarageDoorDownCommand garageDoorDown =

              new GarageDoorDownCommand(garageDoor);

 

       StereoOnWithCDCommand stereoOnWithCD =

              new StereoOnWithCDCommand(stereo);

       StereoOffCommand stereoOff =

              new StereoOffCommand(stereo);

 

       remoteControl.setCommand(0, livingRoomLightOn,livingRoomLightOff);

       remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);

       remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff);

       remoteControl.setCommand(3, stereoOnWithCD, stereoOff);

 

       System.out.println(remoteControl);

 

       remoteControl.onButtonWasPushed(0);

       remoteControl.offButtonWasPushed(0);

       remoteControl.onButtonWasPushed(1);

       remoteControl.offButtonWasPushed(1);

       remoteControl.onButtonWasPushed(2);

       remoteControl.offButtonWasPushed(2);

       remoteControl.onButtonWasPushed(3);

       remoteControl.offButtonWasPushed(3);

    }

}

应用command模式的远程控制器类图:

8.    模式应用—undo的实现

前面的实现没有问题,但是我们好像忘了某些东西—undo按钮。按了这个按钮之后,上一次的操作应该回退(相当于要执行一个反向操作)。现在我们先来看看Light的一组command如何实现反向操作的:

public class LightOffCommand implements Command {

    Light light;

    int level;

    public LightOffCommand(Light light) {

       this.light = light;

    }

 

    public void execute() {

        level = light.getLevel();

       light.off();

    }

 

    public void undo() {

       light.dim(level);

    }

}

public class LightOnCommand implements Command {

    Light light;

    int level;

    public LightOnCommand(Light light) {

       this.light = light;

    }

 

    public void execute() {

        level = light.getLevel();

       light.on();

    }

 

    public void undo() {

       light.dim(level);

    }

}

只有这些显然是不够的,要想实现undo按钮的功能,我们还需要在远程控制器里面做些小的改动:记录下最后执行的command才行,当undo按钮按下时调用最后command的undo方法:

public class RemoteControlWithUndo {

    Command[] onCommands;

    Command[] offCommands;

    Command undoCommand;

 

    public RemoteControlWithUndo() {

       onCommands = new Command[7];

       offCommands = new Command[7];

 

       Command noCommand = new NoCommand();

       for(int i=0;i<7;i++) {

           onCommands[i] = noCommand;

           offCommands[i] = noCommand;

       }

       undoCommand = noCommand;

    }

 

    public void setCommand(int slot, Command onCommand, Command offCommand) {

       onCommands[slot] = onCommand;

       offCommands[slot] = offCommand;

    }

 

    public void onButtonWasPushed(int slot) {

       onCommands[slot].execute();

       undoCommand = onCommands[slot];

    }

 

    public void offButtonWasPushed(int slot) {

       offCommands[slot].execute();

       undoCommand = offCommands[slot];

    }

 

    public void undoButtonWasPushed() {

       undoCommand.undo();

    }

 

    public String toString() {

       StringBuffer stringBuff = new StringBuffer();

       stringBuff.append("\n------Remote Control -------\n");

       for (int i = 0; i < onCommands.length; i++) {

           stringBuff.append("[slot " + i + "] " + onCommands[i].getClass().getName()

              + "    " + offCommands[i].getClass().getName() + "\n");

       }

       stringBuff.append("[undo]" + undoCommand.getClass().getName() + "\n");

       return stringBuff.toString();

    }

}

测试下undo功能:

public class RemoteLoader {

 

    public static void main(String[] args) {

       RemoteControlWithUndo remoteControl = newRemoteControlWithUndo();

 

       Light livingRoomLight = new Light("Living Room");

 

       LightOnCommand livingRoomLightOn =

              new LightOnCommand(livingRoomLight);

       LightOffCommand livingRoomLightOff =

              new LightOffCommand(livingRoomLight);

 

       remoteControl.setCommand(0, livingRoomLightOn,livingRoomLightOff);

 

       remoteControl.onButtonWasPushed(0);

       remoteControl.offButtonWasPushed(0);

       System.out.println(remoteControl);

       remoteControl.undoButtonWasPushed();

       remoteControl.offButtonWasPushed(0);

       remoteControl.onButtonWasPushed(0);

       System.out.println(remoteControl);

       remoteControl.undoButtonWasPushed();

 

       CeilingFan ceilingFan = new CeilingFan("Living Room");

  

       CeilingFanMediumCommand ceilingFanMedium =

              new CeilingFanMediumCommand(ceilingFan);

       CeilingFanHighCommand ceilingFanHigh =

              new CeilingFanHighCommand(ceilingFan);

       CeilingFanOffCommand ceilingFanOff =

              new CeilingFanOffCommand(ceilingFan);

 

       remoteControl.setCommand(0, ceilingFanMedium, ceilingFanOff);

       remoteControl.setCommand(1, ceilingFanHigh, ceilingFanOff);

  

       remoteControl.onButtonWasPushed(0);

       remoteControl.offButtonWasPushed(0);

       System.out.println(remoteControl);

       remoteControl.undoButtonWasPushed();

 

       remoteControl.onButtonWasPushed(1);

       System.out.println(remoteControl);

       remoteControl.undoButtonWasPushed();

    }

}

9.    模式应用—带状态的undo

对于Light来说,undo很容易实现,因为只有on、off两个动作,反向动作很明确,那对于有多种动作(或者多种状态)的CeilingFan,假设有高、中、低三种风速,那undo操作如何实现?

我们看看吊扇提供的远程控制接口:

public class CeilingFan {

    public static final int HIGH = 3;

    public static final int MEDIUM = 2;

    public static final int LOW = 1;

    public static final int OFF = 0;

    String location;

    int speed;

 

    public CeilingFan(String location) {

       this.location = location;

       speed = OFF;

    }

 

    public void high() {

       speed = HIGH;

       System.out.println(location + " ceilingfan is on high");

    }

 

    public void medium() {

       speed = MEDIUM;

       System.out.println(location + " ceilingfan is on medium");

    }

 

    public void low() {

       speed = LOW;

       System.out.println(location + " ceilingfan is on low");

    }

 

    public void off() {

       speed = OFF;

       System.out.println(location + " ceilingfan is off");

    }

 

    public int getSpeed() {

       return speed;

    }

}

    我们看看怎么为CeilingFan实现一个undo方法(显然需要记下来前一个风速):

public class CeilingFanHighCommand implements Command {

    CeilingFan ceilingFan;

    int prevSpeed;

 

    public CeilingFanHighCommand(CeilingFan ceilingFan) {

       this.ceilingFan = ceilingFan;

    }

 

    public void execute() {

       prevSpeed = ceilingFan.getSpeed();

       ceilingFan.high();

    }

 

    public void undo() {

       if (prevSpeed == CeilingFan.HIGH) {

           ceilingFan.high();

       } else if (prevSpeed == CeilingFan.MEDIUM) {

           ceilingFan.medium();

       } else if (prevSpeed == CeilingFan.LOW) {

           ceilingFan.low();

       } else if (prevSpeed == CeilingFan.OFF) {

           ceilingFan.off();

       }

    }

}

10.             模式应用—macrocommand

设想下,如果我们想按下一个按钮,实现对多个电器的控制,应该怎么做?

可以将多个command放到一起,形成一个macro command。

public class MacroCommand implements Command {

    Command[] commands;

 

    public MacroCommand(Command[] commands) {

       this.commands = commands;

    }

 

    public void execute() {

       for (int i = 0; i < commands.length; i++) {

           commands[i].execute();

       }

    }

 

    /**

     * NOTE:  these commands have to be done backwards to ensure proper undo functionality

     */

    public void undo() {

       for (int i = commands.length -1; i >= 0; i--) {

           commands[i].undo();

       }

    }

}

看看对macrocommand的应用:

public class RemoteLoader {

 

    public static void main(String[] args) {

 

       RemoteControl remoteControl = new RemoteControl();

 

       Light light = new Light("LivingRoom");

       TV tv = new TV("LivingRoom");

       Stereo stereo = new Stereo("LivingRoom");

       Hottub hottub = new Hottub();

 

       LightOnCommand lightOn = new LightOnCommand(light);

       StereoOnCommand stereoOn = new StereoOnCommand(stereo);

       TVOnCommand tvOn = new TVOnCommand(tv);

       HottubOnCommand hottubOn = new HottubOnCommand(hottub);

       LightOffCommand lightOff = new LightOffCommand(light);

       StereoOffCommand stereoOff = new StereoOffCommand(stereo);

       TVOffCommand tvOff = new TVOffCommand(tv);

       HottubOffCommand hottubOff = new HottubOffCommand(hottub);

 

       Command[] partyOn = { lightOn, stereoOn, tvOn, hottubOn};

       Command[] partyOff = { lightOff, stereoOff, tvOff, hottubOff};

 

       MacroCommand partyOnMacro = new MacroCommand(partyOn);

       MacroCommand partyOffMacro = new MacroCommand(partyOff);

 

       remoteControl.setCommand(0, partyOnMacro, partyOffMacro);

 

       System.out.println(remoteControl);

       System.out.println("--- PushingMacro On---");

       remoteControl.onButtonWasPushed(0);

       System.out.println("--- PushingMacro Off---");

       remoteControl.offButtonWasPushed(0);

    }

}

11.             没有傻问题

1.      为什么总是需要一个receiver?把receiver实现的功能放到command中实现怎么样?

A:一般情况下,我们会致力于让command傻一些,不用了解receiver的实现细节,但是现实中确实存在大量“过于聪明”的command了。如果具体的东西都在command中实现,我们的松散耦合的层次会降低,command也就不再具有参数化的功能。

2.      怎么才能实现多次的undo操作?

A:一步undo的时候记录了上一个执行的command,实现多次undo时,只需将所有执行过的操作都存入栈结构,undo一次,pop出来一个command即可。

12.             命令模式的其他应用

Ø  队列请求

命令模式使用对象封装一个请求的实现(持有一个receiver,调用一组receiver的方法),从而可以为调用者设置不同的请求。现实中有一种场景,该请求不是立即满足的,可能要在客户端创建命令后很长一段时间才调用,还可能由另外的线程调用。现实中有很多这样的场景,比如任务调度器、线程池、任务队列等。

在任务队列的一端增加不同的command,在任务队列的另一端,挂着一堆执行线程,只要用空闲线程就从任务队列中取出一个comand执行;执行线程不了解command的具体内容,只知道每个command都有一个execute方法,无论哪一种command(执行计算或者进行网络连接),对执行线程都一样,只是简单调用execute。

Ø  日志问题

存在这样的场景:我们希望在系统出故障时恢复到之前的状态(或某一个安全的检查点),应用命令模式很自然地想到要保存之前所有执行过的command,再对每个执行过的command按照执行顺序再执行一遍即可。

在command中扩展两个方法:store方法用于保存执行过的command(当然应该进行持久化的保存),load方法用于重新执行该方法。

13.             Review

新模式:

The CommandPattern encapsulates a request as an object, thereby letting you parameterizeother objects with different requests, queue or log requests, and support undoableoperations.

命令模式将请求封装成对象,从而实现应用不同请求参数化调用者,使用同一个调用者可以实现多种不同的请求,方便实现队列、日志以及系统恢复。

模式回顾:

1.      命令模式实现请求者与具体命令实现者之间的松散耦合。

2.      命令模式的核心是Command,它封装了对命令具体实现的封装。

3.      Invoker调用Command的execute方法发出请求,Command调用Receiver的方法实现请求。

4.      Invoker可以用不同的Command实现运行期的参数化。

5.      Command通过提供undo方法和保存上一个执行的command实现undo功能。

6.      Macro Command可以一次执行多个command,也很容易实现undo。

7.      通常情况下,我们使用“傻”command来实现更好的封装和松散耦合。

8.      命令模式还可用于实现任务队列及系统恢复。

OO准则:

a. 封装变化,encapsulate what varies

b. 组合优于继承, favorcomposition over inheritance

c. 面向接口编程,program to interfaces, not implementation

d. 致力于实现交互对象之间的松散耦合, strive for loosely coupled designs between objects that interact

e. 类应该对于扩展开发,对于修改封闭, classes should be open for extension but closed for modification

f. 依赖于抽象类或者接口而不是具体类。Depend on abstraction. Do not depend on concrete classes.

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值