向没有开辟的领域进军,才能创造新天地。——[美]李政道

命令模式的最大优势为将应用分层处理,避免各层耦合

这里举个例子

在市中心逛了很久的街后, 你找到了一家不错的餐厅, 坐在了临窗的座位上。 一名友善的服务员走近你, 迅速记下你点的食物, 写在一张纸上。 服务员来到厨房, 把订单贴在墙上。 过了一段时间, 厨师拿到了订单, 他根据订单来准备食物。 厨师将做好的食物和订单一起放在托盘上。 服务员看到托盘后对订单进行检查, 确保所有食物都是你要的, 然后将食物放到了你的桌上。

那张纸就是一个命令, 它在厨师开始烹饪前一直位于队列中。 命令中包含与烹饪这些食物相关的所有信息。 厨师能够根据它马上开始烹饪, 而无需跑来直接和你确认订单详情。

我们如果不使用命令模式,写出来的代码就如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void withoutCommandMode() throws InterruptedException {
// 在市中心逛了很久的街后, 你找到了一家不错的餐厅, 坐在了临窗的座位上。
// 一名友善的服务员走近你, 迅速记下你点的食物, 写在一张纸上。
List<String> order = Arrays.asList("牛排", "意面", "芭菲", "拉菲");
// 服务员来到厨房, 把订单贴在墙上。
System.out.println("处理订单");
System.out.println(order);
// 过了一段时间, 厨师拿到了订单, 他根据订单来准备食物。
Thread.sleep(1000);
// 厨师将做好的食物和订单一起放在托盘上。
// 服务员看到托盘后对订单进行检查, 确保所有食物都是你要的, 然后将食物放到了你的桌上。
System.out.println("检查清单");
System.out.println(order);
}

这样写的坏处大大滴,因为我们不便于拓展,而且处理订单的逻辑和检查清单的逻辑耦合在一起了,那么我们这里使用命令模式去重构代码

这里明确几个角色:

  1. 发送者:此处就是顾客,在本次例子中我们省略掉了,关于发送者的定义为:

    发送者 (Sender)——亦称 “触发者 (Invoker)”——类负责对请求进行初始化, 其中必须包含一个成员变量来存储对于命令对象的引用。 发送者触发命令, 而不向接收者直接发送请求。 注意, 发送者并不负责创建命令对象: 它通常会通过构造函数从客户端处获得预先生成的命令。

  2. 命令:此处就是纸(Paper抽象类),我们将执行命令这一操作抽取出来,并且不给它具体参数,让它从预先定义或者让其能够自行获取数据,命令的定义:

    命令 (Command) 接口通常仅声明一个执行命令的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.ruben.simplescaffold.desgin.behavior.command.demo.restaurant;

import lombok.AllArgsConstructor;

/**
* 那张纸就是一个命令, 它在厨师开始烹饪前一直位于队列中。
* 命令中包含与烹饪这些食物相关的所有信息。
* 厨师能够根据它马上开始烹饪, 而无需跑来直接和你确认订单详情。
*
* @author <achao1441470436@gmail.com>
* @since 2021/12/5 0005 16:11
*/
@AllArgsConstructor
public abstract class Paper {

public Waiter waiter;

/**
* 执行纸上的命令
*/
public abstract void execute();

}
  1. 具体命令:此处就是PreparePaperProcessorPaper,就是Paper的具体实现,定义:

    具体命令 (Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务逻辑对象。 但为了简化代码, 这些类可以进行合并。

    接收对象执行方法所需的参数可以声明为具体命令的成员变量。 你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.ruben.simplescaffold.desgin.behavior.command.demo.restaurant;

/**
* 拿到厨房去的纸
*
* @author <achao1441470436@gmail.com>
* @since 2021/12/5 0005 16:19
*/
public class PreparePaper extends Paper {

public PreparePaper(Waiter waiter) {
super(waiter);
}

/**
* 执行纸上的命令
*/
@Override
public void execute() {
// 处理订单,拿到厨房去
waiter.handleOrder();
}
}

以及

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.ruben.simplescaffold.desgin.behavior.command.demo.restaurant;

/**
* 拿到顾客桌上的清单,和食物一起放到顾客桌上
*
* @author <achao1441470436@gmail.com>
* @since 2021/12/5 0005 16:21
*/
public class ProcessorPaper extends Paper {

public ProcessorPaper(Waiter waiter) {
super(waiter);
}

/**
* 执行纸上的命令
*/
@Override
public void execute() {
// 服务员对订单进行检查, 确保所有食物都是顾客要的, 然后将食物放到顾客的桌上
waiter.checkList();
}
}
  1. 接收者,这里就是我们的服务员(Waiter类),定义:

    接收者 (Receiver) 类包含部分业务逻辑。 几乎任何对象都可以作为接收者。 绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作。

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
package com.ruben.simplescaffold.desgin.behavior.command.demo.restaurant;

import java.util.List;

import lombok.AllArgsConstructor;

/**
* 服务员,接受命令的接收者
*
* @author <achao1441470436@gmail.com>
* @since 2021/12/5 0005 16:23
*/
@AllArgsConstructor
public class Waiter {

public List<String> order;

public void handleOrder() {
// 处理订单
System.out.println("处理订单");
System.out.println(order);
}

public void checkList() {
// 检查清单
System.out.println("检查清单");
System.out.println(order);
}
}
  1. 客户端,这里就是启动类,定义:

    客户端 (Client) 会创建并配置具体命令对象。 客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。 此后, 生成的命令就可以与一个或多个发送者相关联了。

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
package com.ruben.simplescaffold.desgin.behavior.command.demo.restaurant;

import java.util.Arrays;
import java.util.List;

/**
* 在市中心逛了很久的街后, 你找到了一家不错的餐厅, 坐在了临窗的座位上。
* 一名友善的服务员走近你, 迅速记下你点的食物, 写在一张纸上。
* 服务员来到厨房, 把订单贴在墙上。
* 过了一段时间, 厨师拿到了订单, 他根据订单来准备食物。
* 厨师将做好的食物和订单一起放在托盘上。
* 服务员看到托盘后对订单进行检查, 确保所有食物都是你要的, 然后将食物放到了你的桌上。
*
* @author <achao1441470436@gmail.com>
* @since 2021/12/5 0005 16:09
*/
public class RestaurantApplication {

public static void main(String[] args) throws InterruptedException {
useCommandMode();
}

private static void useCommandMode() throws InterruptedException {
// 在市中心逛了很久的街后, 你找到了一家不错的餐厅, 坐在了临窗的座位上。
// 一名友善的服务员走近你, 迅速记下你点的食物, 写在一张纸上。
Waiter waiter = new Waiter(Arrays.asList("牛排", "意面", "芭菲", "拉菲"));
Paper paper = new PreparePaper(waiter);
// 服务员来到厨房, 把订单贴在墙上。
paper.execute();
// 过了一段时间, 厨师拿到了订单, 他根据订单来准备食物。
Thread.sleep(1000);
// 厨师将做好的食物和订单一起放在托盘上。
// 服务员看到托盘后对订单进行检查, 确保所有食物都是你要的, 然后将食物放到了你的桌上。
paper = new ProcessorPaper(waiter);
paper.execute();
}

}

命令模式优点:

  • 单一职责原则。 你可以解耦触发和执行操作的类。

  • 开闭原则。 你可以在不修改已有客户端代码的情况下在程序中创建新的命令。

  • 你可以实现撤销和恢复功能。

  • 你可以实现操作的延迟执行。

  • 你可以将一组简单命令组合成一个复杂命令。

命令模式缺点:

  • 代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次。