logo头像

寒墨轩

—— 墨寒的👨‍💻码疯窝

Java设计模式(18)-命令模式

command pattern diancai

(图片来源于网络)

试想一个去餐厅吃饭的场景,假设餐厅还没有智能化的系统,流程基本上是这样:客人坐下后,服务员会拿出菜单让客人点菜,每点一个菜品就在小本上记录下来,等客人点餐完毕后,服务员将所记录的客人已点菜品清单交给厨房,由厨师来负责烹饪。等待客人用菜完成后,服务员可以拿出记录的点菜清单,然后交给餐厅前台收银员收款结账。这整个过程中,有这么几个角色:客人、服务员、厨师、收银员,现在我们用代码来描述这个场景,怎么来设计呢?

1. 传统的点菜系统设计

传统的方式来设计,我们可能会很容易想到如下的类图关系:

command pattern without
Figure 1. 传统方式设计的点菜系统类图

客人直接依赖服务员,使用其点菜服务来点菜,而服务员直接依赖厨师类,通知其完成做菜功能。客人用餐完成后,可以去前台结账。服务员所记录的点菜清单很好的为厨师和收银人员提供了依据:厨师可以借助点菜清单知道自己需要做什么菜,收银人员可以借助点菜清单来计算收款数目。

整个设计初看没有问题,各个功能都实现并且运行的很好。但是,如果某些客人点完菜过后发现,自己点的菜品太多了,可能吃不完而造成粮食的浪费。我们需要系统支持客人取消点菜的功能,一旦客人发现菜品过剩,就可以申请取消,以发扬勤俭节约的美德。

好吧,我们只需要在服务员类上添加"取消点菜"的功能,当服务员收到取消点菜的请求时,他可以在点菜清单上将取消的菜品划掉,然后再交给厨师一个新的点菜清单。此时的类图看起来像这样:

command pattern without2
Figure 2. 添加了取消点菜功能的类图设计

虽然功能实现了,但是客人、服务员和厨师三者的关系是紧耦合的,随着客人需求的更改,服务员和厨师都需要更改。如果厨师有多个人,每个人负责不同的菜品类别,如炒菜厨师只负责炒菜,而蒸菜、汤品、面食都由不同的厨师负责。那么,客人所点的菜品可能会由多个厨师来完成制作,此时服务员可能会手忙脚乱,因为服务员与厨师紧耦合,客人新增点菜、取消点菜等等请求都需要服务员去与厨师交涉,一旦弄错了对应关系就会造成严重的后果。

其实,服务员与厨师的关系可能是通知和被通知的关系,如果能将这种关系抽象出来,那么服务员和厨师的关系不就解耦了吗?可以将服务员通知厨师看成是给厨师下达一个命令,这个命令有多种支持的操作,如:执行命令、撤销命令、重做命令等等,执行命令时,可以记录日志,以便查阅,就如同服务员手上的"点菜清单",随时都能查看客户点了什么菜、取消了什么菜。

服务员发出命令,我们称之他为请求者,而厨师我们称之为接收者,这种将命令请求者和命令接收者之间添加一层命令抽象的设计模式就是本文要讨论的命令模式。

2. 什么是命令模式

命令模式:将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化,并且可以对请求排队或记录日志,还支持可以撤销的操作。

command pattern class
Figure 3. 命令模式类图

从图上可以看出,命令模式有几个角色:

  • 请求者(Invoker): 请求的发起者,内部聚合了很多命令对象,可以调用这些命令对象执行相关请求,但是不直接依赖接收者(Receiver);

  • 抽象命令(Abstract Command): 定义了命令通用方法的接口或者抽象类(通常包含execute方法表示执行命令),如执行、撤销、重做等;

  • 具体命令(Concrete Command): 实现或者继承抽象命令,但是自己不直接实现业务逻辑,而是内部聚合多个Receiver,调用其相关方法来完成命令的具体业务实现;

  • 接收者(Receiver): 真正实现了命令的相关业务功能,被具体命令对象依赖;

命令模式常见的场景

  1. 遥控器,可以控制家用电器的开关、模式切换、频道切换等,可以使用命令模式来设计,遥控器作为请求者(Invoker),电器作为接收者(Receiver);

  2. 前文所述的到餐厅点菜,就可以使用命令模式设计,服务员作为请求者(Invoker),而厨师作为命令接收者(Receiver),抽象命令定义点菜、取消点菜等方法;

  3. GUI软件系统界面功能的设计,界面包含多个功能按钮,点击后分别实现不同的功能,此时可以使用命令模式,按钮作为请求者(Invoker),系统后端实现按钮点击后真正功能的类作为接收者(Receiver),而抽象命令可以定义按钮的执行方法,如下图所示:

command pattern gui button
Figure 4. GUI对象将命令委派给命令对象
命令模式的优点
  1. 完全符合单一职责原则,每一个具体命令(Concrete Command)负责单独的命令逻辑;

  2. 完全符合开闭原则,可以在不修改已有客户端代码的情况下在程序中扩展新的命令;

  3. 通过引入中间层(命令层)降低了系统的耦合度,便于命令的扩展;

  4. 可以实现命令的撤销和恢复功能;可以实现命令的排队和延迟执行;

  5. 可以实现将多个命令组合成复杂的命令。

命令模式的缺点
  1. 增加了命令层,系统复杂度增加,带来系统理解上的困难;

  2. 具体命令类的数量增加导致系统的类数量增加,系统的开发和维护成本增高。

3. 命令模式代码

接下来看看命名模式的基本代码实现:

抽象命名接口(Abstract Command):
1
2
3
interface Command {
void execute();
}
请求者(Invoker):
1
2
3
4
5
6
7
8
9
10
11
class Invoker {
private Command command;

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

public void executeCommand() {
this.command.execute();
}
}
1 调用者聚合抽象命令类
接收者(Receiver)
1
2
3
4
5
class Receiver {
public void action() {
System.out.println("执行命令");
}
}
具体命令(Concrete Command)
1
2
3
4
5
6
7
8
9
10
11
12
class ConcreteCommand implements Command {
private Receiver receiver;

public void setReceiver(Receiver receiver) { (1)
this.receiver = receiver;
}

@Override
public void execute() {
this.receiver.action();
}
}
1 具体命令聚合接收者

以上代码就是命令模式的基本实现结构,类与类之间的依赖关系可以设计为聚合和组合关系(set方法传值和构造器传值),这里的代码设计的是聚合关系。

4. 用命令模式的点菜系统设计

现在,我们用设计模式来重新设计本文开篇的点菜系统,改造后的类图如下:

command pattern diancai class
Figure 5. 使用命令模式设计的点菜系统类图

如图所示,改造后的设计,将服务员(Waiter)和厨师(Cook)完全解耦,而增加一个命令层,将点菜、取消点菜等抽象到命令层。这样一来,即使添加新的菜品,或者不同的菜品由不同的厨师负责,也可以很容易在命令层上扩展多个具体点菜命令,然后增加多个厨师类即可,客户端的代码不需要做任何改动。

关键示例代码如下:

1、点菜命令:

1
2
3
4
5
6
7
8
// 点餐命令
interface OrderCommand {
// 点餐
void execute(String... names);

// 取消点餐
void cancel(String... names);
}

2、服务员:

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
// 餐厅服务员
class Waiter {
private OrderCommand orderCommand;
private final List names = new ArrayList<>();
private final List canceledNames = new ArrayList<>();

public void setOrderCommand(OrderCommand orderCommand) {
this.orderCommand = orderCommand;
}

// 点菜服务
void orderService(String name) {
System.out.println("客户点了菜品:" + name);
names.add(name);
}

// 取消点菜
void cancelOrder(String name) {
System.out.println("客户取消菜品:" + name);
canceledNames.add(name);
}

// 通知点餐完成
void orderFinished() {
System.out.println("点餐完成,交给厨房做菜");
this.orderCommand.execute(names.toArray(new String[]{}));
}

// 通知取消点餐
void cancelFinished() {
System.out.println("通知厨房客户取消了菜品:" + names);
this.orderCommand.cancel(canceledNames.toArray(new String[]{}));
}
}

3、厨师

1
2
3
4
5
6
7
8
9
10
// 厨师,可以细化为多种厨师,负责实现某一具体命令,如炒菜厨师、蒸菜厨师、做汤的初始等等
class Cook {
void cooking(String name) {
System.out.println("厨师正在做菜: " + name);
}

void knownCancel(String... names) {
System.out.println("知道客户取消了,不再做:" + Arrays.toString(names));
}
}

4、具体点菜命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 具体点餐命令,可以视情况细化,比如将点菜命令细化为炒菜命令、做汤命令、蒸菜命令等
class ConcreteOrderCommand implements OrderCommand {
private Cook cook;

public void setCook(Cook cook) {
this.cook = cook;
}

@Override
public void execute(String... names) {
// 厨师按顺序做菜
for (String name : names) {
this.cook.cooking(name);
}
}

@Override
public void cancel(String... names) {
this.cook.knownCancel(names);
}
}

5、客户端调用代码如下:

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 CommandPatternDemo {
public static void main(String[] args) {
Cook cook = new Cook();
ConcreteOrderCommand orderCommand = new ConcreteOrderCommand();
orderCommand.setCook(cook);
Waiter waiter = new Waiter();
waiter.setOrderCommand(orderCommand);

System.out.println("= 客户:服务员,我要开始点菜了...");
System.out.println("= 服务员:欢迎光临,请点菜,这是菜单");
waiter.orderService("回锅肉");
waiter.orderService("青椒土豆丝");
// 各种点菜……
System.out.println("= 服务员(暗):猪啊,吃这么多,管他呢,完事!");
waiter.orderFinished();

System.out.println("= 客户(暗):遭了,点多了");
System.out.println("= 客户:服务员,我要取消这几个菜!");
waiter.cancelOrder("回锅肉");
waiter.cancelOrder("蚂蚁上树");
waiter.cancelOrder("双龙戏珠");
waiter.cancelFinished();
System.out.println("= 厨师(暗):你大爷,做好了又不要,瞎折腾!");
}
}

以上完整示例代码见 github

5. 总结

命令模式通过引入一个命令层将请求发起者和接收者解耦,便于扩展更多的命令,其核心在于命令的扩展。由于命令模式会增加一个层次结构,给系统来带复杂性,因此,命令应该是是否使用命令模式考虑的关键因素。如果存在多个功能相同的逻辑,可以进行命令抽象,而且具体的业务实现并不是本身而是需要依赖其他对象,还需要支持撤销、重做等相关复杂逻辑时,才考虑使用命令模式,否则往往会达不到预期效果。

支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励