设计模式之命令模式

《Head First 设计模式》阅读笔记

Posted by wangtiegang on November 3, 2019

命令模式的概念有点抽象,书上的定义为

命令模式:将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。

在软件系统中,“动作的请求者”与“动作的执行者”通常是一种紧耦合的关系,但某些场合,比如需要对“动作”进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适,命令模式将“动作的请求者”从“动作的执行者”对象中解藕,“请求者”只管发出请求,而不关心请求对于的具体“动作”是什么。

命令模式定义了几种角色

  • Command

    命令对象,提供一个 execute() 方法,这个方法封装了具体的动作调用,调用这个方法就会调用接收者的动作。

  • Invoker

    调用命令的入口,使用 setCommand() 方法,传人具体的命令对象。

  • Receiver

    接收者,真正的命令执行对象。

书上使用了一个遥控器的例子来讲解,假设有一个多功能遥控器,有7排按钮,每排按钮可以控制一种电器的开和关,比如灯,冰箱,电视等。那要怎么设计这个遥控器的代码呢?如果接收到遥控器按钮的信号后,判断是哪个按钮信号,就调用具体电器的调用代码,就会变成下面的样子

1
2
3
4
5
6
7
if (btnIndex == 1){
    Light.on();
}else if(btnIndex == 2){
    Fridge.on();
}else if(btnIndex ==3){
    Television.on();
}

显然这样的代码是很不好维护和扩展的,如果后续增加一种电器,就需要修改一次调用代码,这样会造成潜在的错误,而且没完没了。

如果使用命令模式,就可以改善这样的情况,把请求(例如开灯)封装成一个命令对象,每个按钮都储存一个这样的对象,当按钮被按下的时候,命令被发送出去,接收者收到命名之后执行,就可以让命令对象做相关的工作。在这个过程中,遥控器并不需要知道命令对象的具体动作是什么,之是发送了一个命令,这样就将遥控器和开灯这样的动作解藕了。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* 命令对象的接口
*/
public interface Command {
    public void execute();
}

/**
* Command
* 电灯打开命令
*/
public class LightOnCommand implements Command {

    /**
    * Receiver
    * 电灯对象,真正的命令执行对象
    */
    Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }

}

/**
* Invoker
* 传入具体命令并调用
*/
public class RemoteControl {
    
    /**
    * 持有一个命令
    */
    Command command;

    public RemoteControl() {}

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

    /**
    * 当按钮按下时,调用命名
    */
    public void buttonWasPressed() {
        command.execute();
    }

}

/**
* 测试
*/
public class RemoteControlTest {
    
    public static void main(String[] args) {

        // 创建一个调用者(遥控器)
        RemoteControl remote = new RemoteControl();
        // 创建一个接收者
        Light light = new Light();
        // 创建一个命令,将接收者传给命令
        LightOnCommand command = new LightOnCommand(light);

        // 将命令传个调用者(存储在一个按钮中)
        remote.setCommand(command);
        // 按钮按下,发送请求
        remote.buttonWasPressed();

    }

}

上面的代码就演示了如何实现一个命令模式,调用者设置好命令之后,调用命令,命令被接收者收到后执行。

如果要实现撤销功能,只需要在命令中增加 undo() 方法就好了,调用者记录下最后一个命令,如果要撤销就调用它的 undo() 方法,如果要撤销很多步骤,则需要使用栈记录所以命令,依次取出撤销。这对事务处理非常有用,要么全部成功,要么全部撤销。命令模式也可以应用在系统操作日志中,比如数据库每次执行命令,没法快速的存储所有命令,设立检查点,将所有操作命令日志记录下来,一旦宕机,重启后从读取点开始执行所有命令,将系统状态恢复到宕机前。