0%

五、命令模式

示例说明

设计一个家电自动化遥控器的api,这个遥控器具有7个可编程插槽(每个都可以指定到一个不同的家电装置),每个插槽都有对应的开关按钮。这个遥控器还具备一个整体的撤销按钮。
现有一组java类,这些类是由多家厂商开发出来的,用来控制家电自动化装置。希望你创建一组控制遥控器的api,让每个插槽都可以控制一个或者一组装置。注意,能够控制目前的装置和任何未来可能出现的装置,这一点很重要。

29832103

厂商类:接口各有差异
29965408

简单介绍命令模式

以餐厅为例,研究顾客,女招待,订单以及快餐厨师之间的交互。
30383427

让我们更详细的研究这个交互过程
30535334

一张订单封装了准备餐点的请求
把订单想象成一个用来请求准备餐点的对象,和一般的对象一样,订单对象可以被传递:从女招待传递到订单柜台。订单接口只包含一个方法:orderUp()。这个方法封装了准备餐点所需的动作。订单内有一个到“需要进行准备工作的对象”(厨师)的引用。这一切都被封装起来了,所以女招待不需要知道订单上有什么,也不需要要知道是谁来准备餐点。她只需要将订单放到订单窗口,然后喊一声“订单来了”。
女招待的工作是接受订单,然后调用订单orderUp()方法
女招待的工作很简单:接下顾客的订单,继续帮助下一个顾客,然后将一定数量订单放到订单柜台,并调用orderUp()方法,让人准备餐点。
现在,一天内,不同的顾客有不同的订单,这会使得女招待的takeOrder()方法传入不同的参数。女招待知道所有的订单都支持orderUp方法。
快餐厨师具备准备餐点的知识
快餐厨师是一个对象,他真正知道如何准备餐点。一旦女招待调用orderUp方法,快餐厨师就接手,实现需要创建餐点的所有方法。请注意,女招待和厨师之间彻底解耦:女招待的订单封装了餐点的细节,她只要调用每个订单的方法即可,而厨师看了订单就知道该做些什么餐点;厨师和女招待之间从来都不需要直接沟通。

75856343

第一个命令对象

命令接口
所有的命令对象都实现都实现了相同的包含一个方法execute()的接口。

1
2
3
public interface Command {
void execute();
}

实现一个打开电灯的命令

1
2
3
4
5
6
7
8
9
10
11
public class LightOnCommand implements Command {

private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}

使用命令对象
假设遥控器只有一个按钮和对应的插槽,可以控制一个装置。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SimpleRemoteControl {

private Command command;
public void setCommand(Command command) {
this.command = command;
}
/**
* 按钮被按下
*/
public void buttonWasPressed(){
command.execute();
}
}

定义命令对象

将“请求”封装成对象,以便使用不同的请求队列或日志来参数化其他对象。命令模式也支持可撤销的操作
我们知道一个命令对象通过在特定的接收者上绑定一组动作来封装一个请求。命令对象将动作和接收者包进对象中。这个对象只暴露出一个execute()方法,当该方法被调用,接收者就会执行对应的动作。
78928316

将命令指定到插槽

我们打算将遥控器的每个插槽对应到一个命令,这样就让遥控器变成了调用者。当按下按钮,相应的命令对象的execute()方法就会被调用,其结果就是,接收者(电灯,音响等)的动作被执行。
79501931

实现音响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class RemoteControl {

Command[] onCommands;
Command[] offCommands;
/**
* 有7个插槽
* 默认都是空命令对象
*/
public RemoteControl(){
onCommands = new Command[7];
offCommands = new Command[7];
NoCommand noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
/**
* 设置命令
* @param slot 槽位
* @param onCommand 打开命令
* @param offCommand 关闭命令
*/
public void setCommand(int slot, Command onCommand, Command offCommand){
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
/**
* 按下打开按钮
* @param slot 槽位
*/
public void onButtonPushed(int slot){
onCommands[slot].execute();
}
/**
* 按下关闭按钮
* @param slot 槽位
*/
public void offButtonPushed(int slot){
offCommands[slot].execute();
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
RemoteControl remoteControl = new RemoteControl();
Light light = new Light();
Stereo stereo = new Stereo();
GarageDoor garageDoor = new GarageDoor();
LightOnCommand lightOnCommand = new LightOnCommand(light);
LightOffCommand lightOffCommand = new LightOffCommand(light);
GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
GarageDoorCloseCommand garageDoorCloseCommand = new GarageDoorCloseCommand(garageDoor);
StereoOnWithCdCommand stereoOnWithCdCommand = new StereoOnWithCdCommand(stereo);
StereoOffCommand stereoOffCommand = new StereoOffCommand(stereo);
remoteControl.setCommand(0, lightOnCommand, lightOffCommand);
remoteControl.setCommand(1, garageDoorOpenCommand, garageDoorCloseCommand);
remoteControl.setCommand(2, stereoOnWithCdCommand, stereoOffCommand);
System.out.println(remoteControl);
remoteControl.onButtonPushed(0);
remoteControl.onButtonPushed(1);
remoteControl.onButtonPushed(2);
remoteControl.offButtonPushed(0);
remoteControl.offButtonPushed(1);
remoteControl.offButtonPushed(2);
}

实现撤销

现在需要给遥控器加上撤销的功能,比如说电灯现在是关闭的,按下打开按钮后,电灯打开。再按下撤销按钮,上一个动作将被倒转。在这个例子中,电灯将被关闭。

  • 当命令支持撤销时,该命令就必须提供和execute()方法相反的的undo()方法。不管execute()做了什么,undo()都会倒转过来。

    1
    2
    3
    4
    public interface Command {
    void execute();
    void undo();
    }
  • 从lightOnCommand开始,execute是on(),undo()执行的就是off()。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @AllArgsConstructor
    public class LightOnCommand implements Command {
    private Light light;
    @Override
    public void execute() {
    light.on();
    }
    @Override
    public void undo() {
    light.off();
    }
    }
  • 要加上对撤销按钮的支持,我们必须对遥控器类做一些小修改:加入一个新的实例变量,用来追踪最后被调用的命令,然后,不管何时按下撤销按钮,我们都取出这个命令并调用它的undo()方法。

使用状态实现撤销

通常实现撤销功能,需要记录一些状态。已吊扇为例。
需要追踪吊扇最后设置的速度,如果undo()方法被调用了,就恢复成之前吊扇速度的设置值。

  • 增加局部变量以便追踪之前的速度
  • 在execute中,先将之前的状态记录下来,以便需要撤销时使用。
  • undo()将吊扇的速度设置回之前的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public abstract class AbstractCeilingFanCommand implements Command {
protected CeilingFan ceilingFan;
protected int prevSpeed;
public AbstractCeilingFanCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute(){
prevSpeed = ceilingFan.getSpeed();
doExecute();
}
public abstract void doExecute();
@Override
public void undo(){
if (prevSpeed == CeilingFan.HIGH){
ceilingFan.high();
} else if (prevSpeed == CeilingFan.MEDIUM){
ceilingFan.mediun();
} else if (prevSpeed == CeilingFan.LOW){
ceilingFan.low();
} else if (prevSpeed == CeilingFan.OFF){
ceilingFan.off();
}
}
}

每个遥控器都具备“party模式”-宏命令

使用一个宏命令,用来执行一堆命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 宏命令
* @author 陈添明
* @date 2019/1/13
*/
public class MacroCommand implements Command{
Command[] commands;
public MacroCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
for (int i = 0; i < commands.length; i++) {
commands[i].execute();
}
}
@Override
public void undo() {
for (int i = 0; i < commands.length; i++) {
commands[i].undo();
}
}
}

源码:https://github.com/chentianming11/design-pattern
command包!