0%

GoF设计模式(十四):Command Pattern 命令模式

Command Pattern命令模式,作为行为模式的一种,其实现了对命令的请求者与接收者的解耦

abstract.jpeg

引子

在谈到命令模式之前,我们先来看一个简单的小例子。这里我们有两个电器——Tv电视、Sound音响,同时期望通过一个Remote Control遥控器实现对这两个电器的控制(开、关)。两个电器的实现如下,它们可以被视为命令的接收者,用于接收命令并具体执行命令

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 class Tv {
public void turnOn() {
System.out.println("打开电视");
}

public void turnOff() {
System.out.println("关闭电视");
}
}
...
/**
* 接收者: 音响
*/
public class Sound {
public void turnOn() {
System.out.println("打开音响");
}

public void turnOff() {
System.out.println("关闭音响");
}
}

现在再来实现一个遥控器类,即所谓命令的请求者(或称之为调用者)。其实现如下,可以看到基本逻辑还是很清晰的,即通过不同的参数来调用、请求不同的家电控制命令

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
/**
* 调用者: 遥控器
*/
public class RemoteControl {
private Sound sound;
private Tv tv;

public void setSound(Sound sound) {
this.sound = sound;
}

public void setTv(Tv tv) {
this.tv = tv;
}

public void button(int num) {
switch (num) {
case 1:
sound.turnOn(); // 打开音响
break;
case 2:
sound.turnOff(); // 关闭音响
break;
case 3:
tv.turnOn(); // 打开电视
break;
case 4:
tv.turnOff(); // 关闭电视
break;
default:
}
}
}

至此,隔壁老王就通过这个遥控器向家电下达控制命令了

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
/**
* 传统的未使用命令模式的Demo
*/
public class Demo1 {
public static void main(String[] args) {
Sound sound = new Sound();
Tv tv = new Tv();

RemoteControl remoteControl = new RemoteControl();
remoteControl.setSound(sound);
remoteControl.setTv(tv);

System.out.println("传统的未使用命令模式的Demo");
// 通过遥控器发出家电的控制命令
// 打开电视
System.out.println("\n--------------- Test 1: 打开电视 ---------------");
remoteControl.button(3);

// 打开音响
System.out.println("\n--------------- Test 2: 打开音响 ---------------");
remoteControl.button(1);

// 关闭电视
System.out.println("\n--------------- Test 3: 关闭电视 ---------------");
remoteControl.button(4);
}
}

figure 1.jpeg

模式思想

上面的那个例子比较简单易懂,但是其弊端同样很明显。命令的请求者(遥控器)与命令的接收者(电视、音响)之间耦合过于紧密。试想如果老王日后买了一个空调,也期望可以通过这个遥控器对它下发命令进行控制,必然需要我们直接修改RemoteControl类的内部逻辑以实现对控制空调的命令请求

为了解决这个问题,就需要引入我们今天的主题了——Command Pattern命令模式。在命令模式中,其通常有四个角色。而这其中的两个角色——请求者(调用者)、接收者,通过上面的例子大家多多少少有了一些理解,具体来说

  • 请求者角色:又被称为调用者角色,其目的用于请求某个命令。具体地,其是通过持有具体命令角色的实例实现请求转发的。即下文的RemoteControl遥控器类
  • 接收者角色:其负责接收命令,并负责具体的执行命令。即上文的Tv电视类、Sound音响类

为了解决上述两个角色在之前设计方案中的强耦合问题,命令模式中还引入了另外两个角色

  • 命令角色:其负责定义具体命令角色中的抽象接口。即下文的Command接口
  • 具体命令角色:其是对命令角色的具体实现。我们知道一个接收者角色通常可以接收多个命令(比如电视可以接收、执行打开、关闭这两个命令),而在一个具体命令角色通常只负责接收者角色的一个命令。具体地,其通过内部持有相应的接收者角色实例,实现将来自调用者角色的请求转发至真正的接收者来执行

实现

当然,如果仅仅使用上面的文字描述这个模式,其实有点晦涩难懂、不好理解。现在就让我们利用命令模式来改造上文引子的示例。这样大家理解起来就容易多了。首先,对于接收者角色Tv电视类、Sound音响类,我们不需要进行修改,继续使用引文示例的实现即可

我们先来定义命令角色——即Command接口。这里,我们只定义了一个execute方法接口。其主要作用即是将请求转发给接收者角色,关于这一点会在下文具体命令角色中进行体现

1
2
3
4
5
6
7
8
9
/**
* 命令角色
*/
public interface Command {
/**
* 执行命令
*/
void execute();
}

好了,现在让我们来实现具体命令角色。对于Tv电视类而言,其可以接收、执行两个命令——打开电视、关闭电视。为此我们来实现电视类的具体命令角色。一般地,一个具体命令角色只负责接收者角色的一个命令。故,这里我们需要实现两个具体命令角色——TurnOnTvCommand 打开电视命令类、TurnOffTvCommand 关闭电视命令类。可以看到在具体命令角色内部会持有接收者角色的实例以实现请求转发

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
/**
* 具体命令角色: 打开电视命令
*/
public class TurnOnTvCommand implements Command {
private Tv tv;

public void setTv(Tv tv) {
this.tv = tv;
}

@Override
public void execute() {
tv.turnOn();
}
}
...
/**
* 具体命令角色: 关闭电视命令
*/
public class TurnOffTvCommand implements Command {
private Tv tv;

public void setTv(Tv tv) {
this.tv = tv;
}

@Override
public void execute() {
tv.turnOff();
}
}

同理,对于接收者角色音响类而言,我们也有以下两个具体命令角色类——TurnOnSoundCommand 打开音响命令、TurnOffSoundCommand 关闭音响命令

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
/**
* 具体命令角色: 打开音响命令
*/
public class TurnOnSoundCommand implements Command {
private Sound sound;

public void setSound(Sound sound) {
this.sound = sound;
}

@Override
public void execute() {
sound.turnOn();
}
}
...
/**
* 具体命令角色: 关闭音响命令
*/
public class TurnOffSoundCommand implements Command {
private Sound sound;

public void setSound(Sound sound) {
this.sound = sound;
}

@Override
public void execute() {
sound.turnOff();
}
}

当然,在基于命令模式的设计实现下,调用者角色RemoteControl遥控器类也需要重新实现。可以看到,其内部会通过持有一个具体命令角色的实例实现请求的转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 调用者: 遥控器
*/
public class RemoteControl {
private Command command;

public void setCommand(Command command) {
this.command = command;
}

public void button() {
command.execute();
}
}

至此,相信大家应该了解了命令模式的核心内涵了。其是通过 命令角色-具体命令角色 来实现对请求者和接收者之间的解耦。这样日后如果需要拓展对新电器的控制,只需添加新的接收者及相应的具体命令类即可。现在,就让我们来看看隔壁老王如何使用我们的新遥控器来控制家电吧

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
/**
* Command Pattern 命令模式 Demo
*/
public class CommandPatternDemo {
public static void main(String[] args) {

// 将各具体命令角色与相应的接收者进行组装
Tv tv = new Tv();
TurnOnTvCommand turnOnTvCommand = new TurnOnTvCommand();
turnOnTvCommand.setTv(tv);
TurnOffTvCommand turnOffTvCommand = new TurnOffTvCommand();
turnOffTvCommand.setTv(tv);

Sound sound = new Sound();
TurnOnSoundCommand turnOnSoundCommand = new TurnOnSoundCommand();
turnOnSoundCommand.setSound(sound);
TurnOffSoundCommand turnOffSoundCommand = new TurnOffSoundCommand();
turnOffSoundCommand.setSound(sound);

// 构造一个请求者:遥控器
RemoteControl remoteControl = new RemoteControl();

System.out.println("Command Pattern 命令模式 Demo");
// 打开电视
System.out.println("\n--------------- Test 1: 打开电视 ---------------");
remoteControl.setCommand( turnOnTvCommand );
remoteControl.button();

// 打开音响
System.out.println("\n--------------- Test 2: 打开音响 ---------------");
remoteControl.setCommand( turnOnSoundCommand );
remoteControl.button();

// 关闭电视
System.out.println("\n--------------- Test 3: 关闭电视 ---------------");
remoteControl.setCommand( turnOffTvCommand );
remoteControl.button();
}
}

测试结果如下,符合预期

figure 2.jpeg

宏命令

所谓宏命令,其实很简单。之前我们的具体命令角色只负责单个命令。那现在我们能不能把多条命令组合在一起,以便一次请求可以执行多个命令呢?答案当然是可以的——即所谓的宏命令,其内部会持有一个容器用来保存多个具体命令角色的实例,以实现”批量”执行。下面即是一个宏命令的实现——即具体命令角色MacroCommand类

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 class MacroCommand implements Command {
private List<Command> list = new LinkedList<>();

/**
* 设置命令集合
* @param commandList
*/
public void setList(List<Command> commandList) {
list.clear();
list.addAll(commandList);
}

/**
* 执行命令集合中的全部命令
*/
@Override
public void execute() {
for(Command command : list) {
command.execute();
}
}
}

现在,老王就可以通过宏命令这一具体命令角色,实现一次请求达到同时打开电视、音响的目的

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
/**
* Command Pattern 命令模式 Demo
*/
public class CommandPatternDemo {
public static void main(String[] args) {

// 将各命令与相应的接收者进行组装
Tv tv = new Tv();
TurnOnTvCommand turnOnTvCommand = new TurnOnTvCommand();
turnOnTvCommand.setTv(tv);

Sound sound = new Sound();
TurnOnSoundCommand turnOnSoundCommand = new TurnOnSoundCommand();
turnOnSoundCommand.setSound(sound);

// 构造一个请求者:遥控器
RemoteControl remoteControl = new RemoteControl();

System.out.println("Command Pattern 命令模式 Demo");

// 宏命令: 打开电视、音响
System.out.println("\n--------------- Test 4: 宏命令: 打开电视、音响 ---------------");
List<Command> commandList = new LinkedList<>();
commandList.add(turnOnTvCommand);
commandList.add(turnOnSoundCommand);
MacroCommand macroCommand = new MacroCommand();
macroCommand.setList(commandList);

remoteControl.setCommand( macroCommand );
remoteControl.button();

}
}

测试结果如下,符合预期

figure 3.jpeg

参考文献

  1. Head First 设计模式 弗里曼著
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝