设计模式是什么鬼(命令模式)
点击上方“Java知音”,选择“置顶公众号”
技术文章第一时间送达!
//本文作者:凸凹里歐
//本文收录菜单栏:《设计模式是什么鬼》专栏中
命令模式,通常指的是一个对象向另一个对象发送信息指令的行为模型,比如父母命令孩子写作业、将军命令士兵进攻等。我们经过分析拆解方法会得到三个模块,首先得有命令发送方,接着是被传递的命令本身,最后就是命令的接收执行方了。那么,这样拆解到底有什么好处?让我们先来看一个最简单的例子,电灯泡。
既然是电灯那一定对应通电和断电的行为接口了,两个接口方法互斥,我们就叫它Switchable吧。
1public interface Switchable {//电器接口
2 //通电
3 public void on();
4 //断电
5 public void off();
6
7}
对于具体的灯泡实现类,必然是通电亮,断电灭。
1public class Bulb implements Switchable {
2
3
4 public void on(){
5 System.out.println("通电,灯亮。");
6 }
7
8
9 public void off(){
10 System.out.println("断电,灯灭。");
11 }
12
13}
同样地,我们再增加一个设备,如果是风扇的话则是通电转,断电停。
1public class Fan implements Switchable{
2
3
4 public void on() {
5 System.out.println("通电,风扇转动。");
6 }
7
8
9 public void off() {
10 System.out.println("断电,风扇停止。");
11 }
12
13}
我们该如何操作呢?来吧,直接用电线接通电源。
1public class Client {
2
3 public static void main(String[] args) {
4
5 System.out.println("===客户端用【电线】直接操作灯泡===");
6 Bulb bulb = new Bulb();
7 bulb.on();
8 bulb.off();
9 /*打印输出:
10 ===客户端用【电线】直接操作灯泡===
11 通电,灯亮。
12 断电,灯灭。
13 */
14 }
15
16}
也许用户是个糙人,直接用导线给通电了,简单粗暴,虽然没有错,但这看上去与设计模式没有任何瓜葛。为了体现出模式的优越性,我们需要让系统进化得更高级一些,于是我们决定加入另一个模块,开关控制。
1public class Switcher {
2
3 // 此开关与灯耦合,无法替换为其他电器。
4 // private Bulb bulb = new Bulb();
5
6 // 此开关与电器接口耦合,可任意替换电器。
7 private Switchable switchable;
8
9 // 替换电器方法
10 public void setSwitchable(Switchable switchable) {
11 this.switchable = switchable;
12 }
13
14 // 按键事件绑定
15
16 // 按钮“开”按下
17 public void buttonOnClick() {
18 System.out.println("按下开……");
19 switchable.on();
20 }
21
22 // 按钮“关”按下
23 public void buttonOffClick() {
24 System.out.println("按下关……");
25 switchable.off();
26 }
27
28}
这里的开关就类似一个控制器了,有“开”和“关”两个按键分别绑定了设备的“通电”与“断电”行为方法。需要特别注意的是,如果在第4行我们声明地是灯泡,那么无疑这个开关与灯泡就绑定死了,也就是强耦合了,所以第7行我们声明的是Switchable接口引用,并提供第10行的替换电器方法给外界注入任何的设备。好了,我们换个方式运行程序。
1public class Client {
2
3 public static void main(String[] args) {
4
5 System.out.println("===客户端用【开关】操作电器===");
6 Switcher switcher = new Switcher();
7
8 switcher.setSwitchable(new Bulb());//灯泡接入开关。
9 switcher.buttonOnClick();
10 switcher.buttonOffClick();
11 switcher.setSwitchable(new Fan());//风扇接入开关。
12 switcher.buttonOnClick();
13 switcher.buttonOffClick();
14
15 /*打印输出:
16 ===客户端用【开关】操作电器===
17 按下开……
18 通电,灯亮。
19 按下关……
20 断电,灯灭。
21 按下开……
22 通电,风扇转动。
23 按下关……
24 断电,风扇停止。
25 */
26 }
27
28}
这次看上去功能强大多了,开关可以随意地接入灯泡或者风扇,注入的是谁那么开关按钮直接就作用于谁,对于设备我们还可以继续扩展,设计模式开始体现优势了。等等,这个模式好像似曾相识的感觉,没错,这正是之前讲过的策略模式,可这跟命令模式有什么关系?不要着急,我们先看下这个开关策略模式是否满足了我们的需求。
假设我们的设备不断扩展,比如有了电视机,收音机等等设备,它们不止是开关通电这种简单行为模式了,还可以有转换频道、变音量等等更多的行为。
那么我们的简单开关还能满足对接电视机的琳琅满目的功能吗?注意之前我们的开关Switcher类中第7行代码:private Switchable switchable; 虽然可以替换设备,但只能是Switchable设备对象,这就与”可开关设备接口“强耦合了,也就是说它只能控制“灯泡或风扇”,并不能控制”电视或收音机”。
同时另一端我们的开关控制器也在不断进化,发展出了更多功能控制器、无线遥控器、甚至是手机App控制。
所以,如何把控制器与设备完全给拆解开势在必行,此时命令模式粉墨登场。现在我们得新定义出一组”命令“模块把控制器(发令者)与设备(执行者)彻底解耦,就以电视机和遥控器举例说明吧。
1public interface Device extends Switchable{
2
3 // 频道+
4 public void channelUp();
5 // 频道-
6 public void channelDown();
7 // 音量+
8 public void volumeUp();
9 // 音量-
10 public void volumeDown();
11
12}
注意代码第1行的接口继承,我们的高级设备接口则遗传了之前的简单通断电接口,并新增了调节频道和音量4个功能。接下来是电视机与收音机实现类。
1public class TV implements Device {
2
3
4 public void on(){
5 System.out.println("电视机启动");
6 }
7
8
9 public void off(){
10 System.out.println("电视机关闭");
11 }
12
13
14 public void channelUp() {
15 System.out.println("电视机频道+");
16 }
17
18
19 public void channelDown() {
20 System.out.println("电视机频道-");
21 }
22
23
24 public void volumeUp() {
25 System.out.println("电视机音量+");
26 }
27
28
29 public void volumeDown() {
30 System.out.println("电视机音量-");
31 }
32}
1public class Radio implements Device {
2
3
4 public void on(){
5 System.out.println("收音机启动");
6 }
7
8
9 public void off(){
10 System.out.println("收音机关闭");
11 }
12
13
14 public void channelUp() {
15 System.out.println("收音机调频+");
16 }
17
18
19 public void channelDown() {
20 System.out.println("收音机调频-");
21 }
22
23
24 public void volumeUp() {
25 System.out.println("收音机音量+");
26 }
27
28
29 public void volumeDown() {
30 System.out.println("收音机音量-");
31 }
32}
没什么好说的,下来是解耦的重点了,我们在策略模式的基础上又增加一层中间模块,开始编写命令模块代码,首先是命令接口。
1public interface Command {
2
3 //执行命令操作
4 public void exe();
5
6 //反执行命令操作
7 public void unexe();
8
9}
命令接口有执行操作与反执行操作两个标准功能,然后定义其命令实现类,开关机命令、频道转换命令、以及音量调节命令。
1public class SwitchCommand implements Command {
2
3 private Device device;// 此处持有高级设备接口。
4
5 public SwitchCommand(Device device) {
6 this.device = device;
7 }
8
9
10 public void exe() {
11 device.on();// 执行命令调用开机操作
12 }
13
14
15 public void unexe() {
16 device.off();// 反执行命令调用关机操作
17 }
18
19}
1public class ChannelCommand implements Command{
2
3 private Device device;
4
5 public ChannelCommand(Device device) {
6 this.device = device;
7 }
8
9
10 public void exe() {
11 device.channelUp();
12 }
13
14
15 public void unexe() {
16 device.channelDown();
17 }
18
19}
1public class VolumeCommand implements Command{
2
3 private Device device;
4
5 public VolumeCommand(Device device) {
6 this.device = device;
7 }
8
9
10 public void exe() {
11 device.volumeUp();
12 }
13
14
15 public void unexe() {
16 device.volumeDown();
17 }
18
19}
代码很简单,但是系统模组相对复杂,所以一定要搞清楚各模块间关系再继续。最后一个模块是遥控器类,也就是命令发送方了。我们保持简单,遥控器集成了OK按键以及上下左右方向键。
1public class Controller {
2 private Command okCommand;
3 private Command verticalCommand;
4 private Command horizontalCommand;
5
6 // 绑定OK键命令
7 public void bindOKCommand(Command okCommand) {
8 this.okCommand = okCommand;
9 }
10
11 // 绑定上下方向键命令
12 public void bindVerticalCommand(Command verticalCommand) {
13 this.verticalCommand = verticalCommand;
14 }
15
16 // 绑定左右方向键命令
17 public void bindHorizontalCommand(Command horizontalCommand) {
18 this.horizontalCommand = horizontalCommand;
19 }
20
21 // 开始按键映射命令
22 public void buttonOKHold() {
23 System.out.print("长按OK按键……");
24 okCommand.exe();
25 }
26
27 public void buttonOKClick() {
28 System.out.print("单击OK按键……");
29 okCommand.unexe();
30 }
31
32 public void buttonUpClick() {
33 System.out.print("单击↑按键……");
34 verticalCommand.exe();
35 }
36
37 public void buttonDownClick() {
38 System.out.print("单击↓按键……");
39 verticalCommand.unexe();
40 }
41
42 public void buttonLeftClick() {
43 System.out.print("单击←按键……");
44 horizontalCommand.unexe();
45 }
46
47 public void buttonRightClick() {
48 System.out.print("单击→按键……");
49 horizontalCommand.exe();
50 }
51}
这个遥控器持有三个命令组件,并且于第7行开始定义命令绑定方法,最后从第22行开始定义各按键触发方法并映射到相应的命令操作上。可以看到,控制器对设备一无所知,也就是它上面不再绑定有任何设备了,而是只绑定命令。最后,客户端又换了一种方式运行程序。
1public class Client {
2
3 public static void main(String[] args) {
4 System.out.println("===客户端用【可编程式遥控器】操作电器===");
5 Device tv = new TV();
6 Device radio = new Radio();
7 Controller controller = new Controller();
8
9 //绑定【电视机】的【命令】到【控制器按键】
10 controller.bindOKCommand(new SwitchCommand(tv));
11 controller.bindVerticalCommand(new ChannelCommand(tv));//上下调台
12 controller.bindHorizontalCommand(new VolumeCommand(tv));//左右调音
13
14 controller.buttonOKHold();
15 controller.buttonUpClick();
16 controller.buttonUpClick();
17 controller.buttonDownClick();
18 controller.buttonRightClick();
19
20 /*打印输出:
21 ===客户端用【可编程式遥控器】操作电器===
22 长按OK按键……电视机启动
23 单击↑按键……电视机频道+
24 单击↑按键……电视机频道+
25 单击↓按键……电视机频道-
26 单击→按键……电视机音量+
27 */
28
29 //绑定【收音机】的【命令】到【控制器按键】
30 controller.bindOKCommand(new SwitchCommand(radio));
31 controller.bindVerticalCommand(new VolumeCommand(radio));//上下调音
32 controller.bindHorizontalCommand(new ChannelCommand(radio));//左右调台
33
34 controller.buttonOKHold();
35 controller.buttonUpClick();
36 controller.buttonUpClick();
37 controller.buttonRightClick();
38 controller.buttonDownClick();
39
40 /*打印输出:
41 长按OK按键……收音机启动
42 单击↑按键……收音机音量+
43 单击↑按键……收音机音量+
44 单击→按键……收音机调频+
45 单击↓按键……收音机音量-
46 */
47
48 }
49
50}
很显然,客户端可以肆意妄为地组装各个模块了,也就是说可以遥控电视,也可以遥控收音机,或许绑定上下键调音量,或许是换成左右键调音量,甚至可以定义一个宏命令去控制灯泡的切换开关实现一种霓虹灯闪烁的效果(读者可以思考怎样实现),而对于控制器端本身,同样可以继续扩展,或许干脆替换个游戏手柄或者键盘,一样可以发号施令。
至此,发令控制方与接受执行方完全被拆解开,这让我们实现了对各模块的自由扩展,对指令映射、设备绑定的灵活操控,松散的系统得以成就繁多模块解耦的最终目的。