1. 模式定义

命令模式(Command Pattern):==将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开==。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。❕%%
2218-🏡⭐️◼️命令模式的定义?将一个请求封装为对象,调用者和执行者通过命令对象进行沟通,使得发出请求的责任和执行请求的责任分离◼️⭐️-point-202301222218%%

在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可。此时可以使用命令模式来进行设计

2. 模式结构

2.1. ⭐️🔴UML 图示

image.png

2.2. ⭐️🔴角色分析

  • 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法,可以是接口或抽象类。
  • 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作
  • 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
  • 调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口

3. ⭐️🔴实现逻辑

具体命令角色聚合命令接受者:聚合 + 构造导入
调用者 (请求者) 聚合命令接口:聚合 +setter 导入命令并放在集合中 (餐馆堂食) 或者 构造方法中 new 命令放到集合中 (智能生活)
%%
2222-🏡⭐️◼️餐馆堂食和智能生活 2 个案例中命令模式的角色分配及实现逻辑?餐馆堂食:Waiter 是 Invoker,持有 Command,通过 setter 方法 set 到命令集合中。具体的命令 OrderCommand 是具体的命令,持有 SeniorChef(命令接收者),通过构造函数导入。智能家居:RemoteController 是调用者,通过在构造方法中使用 new 关键字创建命令实例放入到命令集合中,并持有这个集合。TVOffCommand 等这种具体的命令,跟餐馆堂食案例一样,也是通过构造导入的方式持有命令接受者,比如对应的 TVReciver。◼️⭐️-point-202301222222%%

4. 案例分析

4.1. 餐馆堂食⭐️🔴

将上面的案例用代码实现,那我们就需要分析命令模式的角色在该案例中由谁来充当。
服务员: 就是调用者角色,由她来发起命令。
资深大厨: 就是接收者角色,真正命令执行的对象。
订单: 命令中包含订单。
类图如下:

4.1.1. 示例代码

[[Waitor.java]]

4.1.2. 实现逻辑

具体命令角色聚合命令接受者:聚合 + 构造导入
调用者 (请求者) 聚合命令接口:聚合 +setter 导入命令并放在集合中

4.2. 智能家居⭐️🔴

image.png

4.2.1. undoCommand

示例代码:[[RemoteController.java]]

4.2.2. 实现逻辑

具体命令角色聚合命令接受者:聚合 + 构造导入
调用者 (请求者) 聚合命令接口:聚合 + 构造方法中 new 命令放到集合中 (智能家居)

5. ⭐️🔴JDK 源码分析

5.1. Runable

Runable 是一个典型命令模式,Runnable 担当抽象命令的角色,Thread 充当的是调用者,持有 Runnable。start 方法就是其执行方法。我们自定义的 TurnOffThread 类属于具体命令角色,持有命令接收者 Reciver。

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
//命令接口(抽象命令角色)
public interface Runnable {
public abstract void run();
}

//调用者
public class Thread implements Runnable {
private Runnable target;

public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();

group.add(this);

boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}

private native void start0();
}

会调用一个 native 方法 start0 (),调用系统方法,开启一个线程。而接收者是对程序员开放的,可以自己定义接收者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* jdk Runnable 命令模式
*
*/
public class TurnOffThread implements Runnable{
private Receiver receiver;

public TurnOffThread(Receiver receiver) {
this.receiver = receiver;
}
public void run() {
receiver.turnOFF();
}
}

5.2. JdbcTemplate ✅

JdbcTemplate 类中 query() 方法中为例,我们可以发现其中定义了一个内部类 QueryStatementCallback,而且 QueryStatementCallback 类实现了 StatementCallback 接口的 doInStatement 方法。这就是命令模式在 Spring 框架 JdbcTemplate 源码中的应用,其中,

  • StatementCallback 充当了 Command 命令,其下有多个实现
  • QueryStatementCallback 充当了 ConcreteCommand 具体的命令角色
  • Statement 充当了 Receiver 接收者角色
  • JdbcTemplate 本身作为调用者

6. 优缺点⭐️🔴

1. 优点:

  • 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。“请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到了纽带桥梁的作用
  • 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。设计一个命令队列。只要把命令对象放到列队,就可以多线程的执行命令
  • 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

2. 缺点:

  • 使用命令模式可能会导致某些系统有过多的具体命令类。
  • 系统结构更加复杂。

7. 适用场景⭐️🔴

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
  • 系统需要在不同的时间指定请求、将请求排队和执行请求
  • 系统需要支持命令的撤销 (Undo) 操作和恢复 (Redo) 操作。

8. 实战经验

  • 空命令也是一种设计模式,它为我们省去了判空的操作。在上面的实例中,如果没有用空命令,我们每按下一个按键都要判空,这给我们编码带来一定的麻烦
  • 命令模式经典的应用场景:界面的一个按钮对应一条命令、模拟 CMD(DOS 命令)订单的撤销/恢复、触发 - 反馈机制

9. 参考与感谢

设计模式-2、设计模式及设计原则