设计模式:命令模式
模式所解决的问题
在前面的文章中,我们讨论过很多的设计模式,比如说 迭代器模式,工厂模式,还有 观察者模式。这些模式都在试图解决代码中的一个核心问题——依赖。什么是依赖呢?就好比一个模块 A 强依赖于模块 B,那么我们在使用模块 A 的同时需要把模块 B 也给搞清楚,还需要弄清楚 A 与 B 之间的关系。当一个系统中的模块数量逐渐增多,并且这些模块都相互依赖,那么我们就需要花更多的时间来维护,而且代码可读性降低,以及添加新功能和模块也变的更加困难。
可能你会说,模块之间本身就需要有依赖和关联啊,这在任何一个应用程序中都是必须的。没错,但是对于一个模块来说,存在着使用者和维护者,对于维护者来说,他需要对这个模块非常了解,了解这个模块和其他模块之间的关系,出了问题也要及时定位到问题,及时修改。但对于使用者来说,他仅仅关心如何才能使用这个模块来达到自己的目的,至于这个模块内部如何实现,如何依赖,他并不关心,如果一个模块能够拿来就用,并且效果和这个模块的名字以及描述一模一样,那就是最好的。
通常来说,降低依赖的一个常规操作就是加一层抽象,这层抽象位于使用者和模块之间,它表示的是一类模块,而不是单一的一个模块。这层抽象定义了某类模块的一些共性,当给到使用者一个模块时,使用者可以把这个模块当成是这个抽象,因而不去关心这个模块是什么,仅仅依据抽象来使用即可。
说了这么多,现在我们来看一个场景,假设我们去餐馆点餐,从点餐到菜品开始制作,这中间的过程是怎么样的呢?不管是点餐员过来帮你点餐,还是通过 APP 点餐,你并没有跟后厨直接交流。看似跟后厨直接点餐是最快捷的方式,其实不然,如果后厨既需要帮你点餐,又要给你介绍菜品,还要制作菜品,先不考虑忙不忙的过来,这种一人兼并数职,一个环节出会影响到其他环节,就好比你点完了菜中途想再增加些菜,那么不好意思,要等后厨忙完才行。所以点餐员或者点餐软件必不可少,这样后厨只需要专注在菜品制作上,你中途想加菜也完全不受影响。
由此一来,下面三个类就是必不可少的了
1
2
3
4
5
6
// Customer
class Client {}
class Waitress {}
class Chef {}
那么这就足够了吗?对于小餐馆来说,也许够了,但是规模稍大一点的餐馆就不行,酒店就更不行了。因为酒店里不止一类厨师,有炒菜的,有做凉菜的,有做面点的,还有做糕点的,再往细了说,每个类别的厨师群体中,又有不同的领域,不同领域中细化到个人,每个人擅长的技能又不一样。当一个点餐员拿到用户的需求后,他如何知道谁才是最合适的人呢?
很明显,现在的问题存在与厨师与点餐员之间,我们需要在点餐员与厨师之间加一层抽象,你可以把它看成是一个统一化的工具,帮助点餐员更好、更快捷地工作。就像下面这样:
1
2
3
4
5
6
7
8
// Customer
class Client {}
class Waitress {}
class Command {}
class Chef {}
加的这个东西就是我们今天要说的主角——命令。
命令模式
通过前面的例子,我们大概理解了什么是命令模式,它具体解决的是什么样的问题。但是到具体的一些细节,我们并没有提及,这里我们就挨个来分析。
之前我们说过的 依赖反转原则——高层模块不要依赖于底层模块,高层模块和底层模块应该通过抽象来实现依赖,而例子对应的实现中,我们并没有引入任何的抽象,这一点就需要进行补充,修改后如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Customer
class Client {}
class Waitress {}
interface Command {}
class TiramisuCommand implements Command {}
class KungPaoChickenCommand implements Command {}
class BraisedPeanut implements Command {}
interface Chef {}
class ChineseChef implements Chef {}
class DessertChef implements Chef {}
class AppetizerChef implements Chef {}
剩下的就是在这些抽象中定义共性的行为了,比如说 Command
对应的菜品,任何一个出现在菜单上的菜品都可以下单,对应来说任何一个命令都是可以被执行的。而任何一个 Chef
都会制作菜品,不管制作的菜品是什么。另外,Command
负责将菜品和厨师关联起来,这样 Chef
只会收到与他相关的菜品制作要求。对于点餐员 Waitress
来说就更简单了,只需要将客户的要求归拢起来,帮助用户下单即可,于是就有以下的修改:
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
// Customer
class Client {}
class Waitress {
List<Command> customerRequests;
public RemoteControl() {
customerRequests = new ArrayList<>();
}
public void setCommand(int slot, Command request) {
customerRequests.add(request);
}
public void makeRequest(int slot) {
customerRequests[slot].execute();
}
}
interface Command {
public void execute();
}
class TiramisuCommand implements Command {
DessertChef chef;
TiramisuCommand(DessertChef chef) {
this.chef = chef
}
public void execute() {
chef.cook();
}
}
class KungPaoChickenCommand implements Command {
ChineseChef chef;
KungPaoChickenCommand(ChineseChef chef) {
this.chef = chef
}
public void execute() {
chef.cook();
}
}
class BraisedPeanutCommand implements Command {
AppetizerChef chef;
BraisedPeanutCommand(AppetizerChef chef) {
this.chef = chef
}
public void execute() {
chef.cook();
}
}
interface Chef {
public void cook();
}
class ChineseChef implements Chef {
public void cook() {}
}
class DessertChef implements Chef {
public void cook() {}
}
class AppetizerChef implements Chef {
public void cook() {}
}
最后,我们还需要在客户端将这些东西组合起来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RestaurantOrderSystem {
Waitress waitress;
public static void initialSetting() {
waitress = new Waitress();
ChineseChef chineseChef = new ChineseChef();
DessertChef dessertChef = new DessertChef();
AppetizerChef appetizerChef = new AppetizerChef();
KungPaoChickenCommand kungPaoChickenCommand = new KungPaoChickenCommand(chineseChef);
TiramisuCommand tiramisuCommand = new TiramisuCommand(dessertChef);
BraisedPeanutCommand braisedPeanutCommand = new BraisedPeanutCommand(appetizerChef);
waitress.setCommand(0, kungPaoChickenCommand);
waitress.setCommand(1, tiramisuCommand);
waitress.setCommand(2, braisedPeanutCommand);
// waitress.makeRequest(0);
// waitress.makeRequest(1);
// waitress.makeRequest(2);
}
}
命令模式对应到的类关系图如下:
这里面,点餐员对应的就是 Invoker
,也就是命令的发起者,厨师对应的就是 Receiver
,也就是命令的接受者,负责命令在执行过程中的各种具体细节。
当然,上面的例子中,我们也仅仅是呈现了一个命令模式的大致框架,其实可以根据实际的情况添加各种的细节,比如下面这些:
- 除了
Receiver
(也就是Chef
),每个命令其实还可以包括更多的细节,对应到我们的例子中,每个命令代表的是一道菜品,那么一些菜品会有一些选项,比如牛排需要几分熟、菜需要少油少盐、加辣、加配菜等等。命令虽说是Waitress
下的,但实际的创建者是顾客,因而这里并没有增加Waitress
和Command
之间的复杂度。 Invoker
(也就是Waitress
)在执行命令时也可以传入一些信息,表示用户的一些额外请求(不在Command
的自带选项中)。Receiver
当中的细节我们都隐去了,一个命令的执行可能很复杂,也可能很简单,但这不是命令模式的重点,Receiver
只需要提供对应的接口给到Command
就行。
命令模式的实际应用
命令模式的核心就是将不同的任务或事物统一封装起来,这样呈现给外界的都是一个个统一类型的对象,让类之间的关系变得更加清晰,降低了模块和模块之间的耦合性。虽说仅仅是简单的封装,但是这在实际中却大有用途。
第一个是关于操作系统的例子,试想一下,一个任务过来,操作系统是如何将这个任务分配到指定的线程的呢?根据职责划分吗,比如一些线程专门负责网络请求,另外一些线程负责程序的编译,还有一些线程负责内存的管理,这样职责清晰,不是很好吗?这其实会造成资源的浪费,比如当网络模块忙碌时,网络模块的资源就会不足,任务就会拥塞,此时其他模块处的资源却非常的充裕,但却不能拿来使用。
更好一点的做法是通过任务队列来实现,每一个进入任务队列的任务都被包装成 Command
,所有的线程都从任务队列中取这些包装好的 Command
。不管是什么样的任务,对任务队列来说都是一样的,任务队列其实只做着一件事情——将任务根据到达先后排序,线程在这个过程中也只做一件事情——当线程空闲,如果队列中还有任务,则将任务从队列中取出。这样,通过命令模式的封装,整个任务的分配这一环节就变的清晰了。
另一个例子是关于日志的,我们知道当系统因为某些故障崩溃,当系统重启后,就需要恢复这段时间系统丢失的数据,而日志在这之中起到关键作用。可这里的问题是,日志如何打才好呢?如果将涉及到的所有数据都放到日志里,那么日志体量将变得巨大,而且有些操作是通过网络进行的(比如去到其他机器上更改数据库),本地根本就无法直接获取到相关的数据。
这里比较好的做法是记录操作,每个系统通常都会有记录点(check point),这表明这个记录点之前的数据,系统都已经持久化完毕了,也就说明系统只要恢复记录点后面的数据即可,最为直接的操作就是直接将相关的操作通过 Command
包装起来,序列化存储到日志中,当系统恢复时,将这些 Command
取出,因为日志已经保证了顺序,因此我们挨个执行一遍就好了。当然了,Command
中可以再添加两个方法,一个是 store()
,另一个是 load()
,整个 Command
接口就变成了下面这样:
1
2
3
4
5
interface Command {
public void execute();
public void store();
public void load();
}
store()
负责的是将相关恢复需要的部分序列化存储起来,load()
则是相反,将实际可执行的对象从序列化后的字符串中恢复出来,有了这两个方法,系统就可以借助命令模式来恢复丢失的数据了。