命令模式

这个模式的核心思想是“请求的封装”,它在实现撤销/重做、任务队列、事务操作等方面发挥着至关重要的作用。

设计模式笔记:命令模式 (Command Pattern)

一、一句话概括

将一个请求封装成一个对象,从而可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

二、为什么需要它 (Why - The Pain Point)

想象一下,你正在设计一个智能家居遥控器。这个遥控器上有很多按钮,每个按钮可以控制不同的设备(灯、风扇、电视等)执行不同的操作(打开、关闭、调高音量等)。

直接实现的糟糕设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 各种设备类
class Light { public void on() { ... } public void off() { ... } }
class Fan { public void high() { ... } public void off() { ... } }

// 痛点:遥控器类与所有设备类紧密耦合
class RemoteControl {
private Light livingRoomLight;
private Fan ceilingFan;

public void onButton1Pressed() {
livingRoomLight.on();
}
public void offButton1Pressed() {
livingRoomLight.off();
}
public void onButton2Pressed() {
ceilingFan.high();
}
// ... 如果有7个按钮,每个按钮都有 on/off,代码会非常臃肿
}

痛点:请求者与接收者的紧密耦合

  1. 高耦合RemoteControl(请求者)必须直接了解并持有所有设备对象(接收者)的引用,并且知道每个设备具体的方法名(on(), high() 等)。
  2. 违反开闭原则:如果要增加一个新的设备(比如音响 Stereo),或者想改变一个按钮的功能(比如让按钮1控制风扇),就必须修改 RemoteControl 的源代码。
  3. 难以实现高级功能
    • **撤销/重做 (Undo/Redo)**:如何记录上一步操作并撤销它?比如按了“开灯”后,如何实现一个“撤销”按钮来关灯?
    • 任务队列:如何将一系列操作(先开灯、再开风扇、再开电视)组合成一个“宏命令”,按一个按钮就全部执行?
    • 日志记录:如何记录所有执行过的操作以便后续审计或重放?

核心问题:如何将“发出请求的对象”和“知道如何执行请求的对象”完全解耦,并将“请求”本身变为一个可操作、可传递、可存储的实体?

三、它是什么 (What - The Solution)

命令模式通过引入命令对象(Command Object) 来解耦这一切。命令对象封装了关于一个操作的所有信息:它应该调用哪个对象哪个方法,以及需要哪些参数

核心思想和角色:

  1. **命令接口 (Command)**:
    • 通常只声明一个核心方法,如 execute()
    • (可选)为了支持撤销,可以再声明一个 undo() 方法。
  2. **具体命令 (Concrete Command)**:
    • 实现了 Command 接口。
    • 持有一个接收者(Receiver)对象的引用
    • execute() 方法的实现就是调用接收者的一个或多个具体方法来完成操作。
    • undo() 方法的实现则是调用接收者的方法来执行与 execute() 相反的操作。
  3. **接收者 (Receiver)**:
    • 这是真正执行业务逻辑的对象。它知道如何完成具体的工作。
    • 例如,Light, Fan 等设备类。
  4. **调用者 (Invoker)**:
    • 它负责触发命令。它持有一个或多个命令对象。
    • 它不关心命令的具体内容,只知道在适当的时候调用命令的 execute() 方法。
    • 例如,遥控器上的按钮、菜单项等。
  5. **客户端 (Client)**:
    • 客户端负责创建具体命令对象,并将其接收者设置好。
    • 然后,客户端将这个配置好的命令对象设置给调用者。

工作流程:

  1. 客户端创建了一个 Light 对象(接收者)和一个 LightOnCommand 对象(具体命令),并将 Light 对象注入到 LightOnCommand 中。
  2. 客户端将 LightOnCommand 设置到遥控器的一个按钮(调用者)上。
  3. 当用户按下该按钮时,调用者(按钮)只管调用它持有的命令对象的 execute() 方法。
  4. LightOnCommandexecute() 方法被触发,它进而调用内部持有的 Light 对象的 on() 方法。

现在,遥控器完全不知道灯的存在,它只认识抽象的 Command 接口。

四、怎么做 (How - The Blueprint)

基本步骤(以智能遥控器为例):

  1. 创建接收者类,如 Light, Fan,包含具体的业务方法。
  2. 创建 Command 接口,包含 execute()undo() 方法。
  3. 创建具体命令类,如 LightOnCommand。它持有 Light 的引用,并实现 execute()(调用 light.on())和 undo()(调用 light.off())。
  4. **创建调用者类 RemoteControl**。它内部可以有一个 Command 数组来对应每个按钮,并提供 setCommand() 方法。当按钮被按下时,调用对应命令的 execute()
  5. 客户端负责组装这一切。

Java 示例代码 (支持撤销)

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// 1. 接收者 (Receiver)
class Light {
private String location;
public Light(String location) { this.location = location; }
public void on() { System.out.println(location + " light is On"); }
public void off() { System.out.println(location + " light is Off"); }
}

// 2. 命令接口 (Command)
interface Command {
void execute();
void undo();
}

// 3. 具体命令 (Concrete Command)
class LightOnCommand implements Command {
private Light light; // 持有接收者的引用

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

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

@Override
public void undo() { light.off(); }
}

class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) { this.light = light; }
@Override
public void execute() { light.off(); }
@Override
public void undo() { light.on(); }
}

// 空命令,用于初始化,避免空指针 (Null Object Pattern)
class NoCommand implements Command {
public void execute() {}
public void undo() {}
}

// 4. 调用者 (Invoker)
class RemoteControl {
private Command[] onCommands;
private Command[] offCommands;
private Command undoCommand; // 记录上一个操作的命令

public RemoteControl() {
onCommands = new Command[7];
offCommands = new Command[7];
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
undoCommand = noCommand;
}

public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}

public void onButtonWasPushed(int slot) {
onCommands[slot].execute();
undoCommand = onCommands[slot]; // 记录
}

public void offButtonWasPushed(int slot) {
offCommands[slot].execute();
undoCommand = offCommands[slot]; // 记录
}


public void undoButtonWasPushed() {
System.out.print("Undo: ");
undoCommand.undo();
}
}

// 5. 客户端
public class CommandPatternDemo {
public static void main(String[] args) {
RemoteControl remote = new RemoteControl();

// 创建接收者
Light livingRoomLight = new Light("Living Room");

// 创建命令,并注入接收者
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);

// 将命令设置给调用者
remote.setCommand(0, livingRoomLightOn, livingRoomLightOff);

// 模拟按键
remote.onButtonWasPushed(0);
remote.offButtonWasPushed(0);

System.out.println("--- Testing Undo ---");
remote.onButtonWasPushed(0);
remote.undoButtonWasPushed(); // 撤销开灯,灯应该会关

remote.offButtonWasPushed(0);
remote.undoButtonWasPushed(); // 撤销关灯,灯应该会开
}
}

五、UML 类图

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
@startuml
skinparam classAttributeIconSize 0
hide empty members

' Command Interface
interface Command {
+ void execute()
+ void undo()
}

' Concrete Commands
class LightOnCommand implements Command {
- Light light
+ void execute()
+ void undo()
}
class LightOffCommand implements Command {
- Light light
+ void execute()
+ void undo()
}
class NoCommand implements Command

' Receiver
class Light {
+ void on()
+ void off()
}

' Invoker
class RemoteControl {
- Command[] onCommands
- Command[] offCommands
- Command undoCommand
+ void setCommand(...)
+ void onButtonWasPushed(int slot)
+ void undoButtonWasPushed()
}

' Client
class CommandPatternDemo {}

' Relationships
' Invoker HAS-A Command
RemoteControl o-- "1..*" Command

' ConcreteCommand HAS-A Receiver
LightOnCommand o-- "1" Light
LightOffCommand o-- "1" Light

' Client creates Receiver, Command and configures Invoker
CommandPatternDemo ..> RemoteControl
CommandPatternDemo ..> Light
CommandPatternDemo ..> LightOnCommand
@enduml

图示解读:

  • RemoteControl (调用者) 拥有 Command 对象。
  • LightOnCommand (具体命令) 拥有 Light 对象 (接收者)。
  • 调用者和接收者之间没有直接联系,通过命令对象解耦。

六、优缺点分析

优点 (Pros):

  1. 解耦:将请求的发送者(调用者)和执行者(接收者)完全解耦。
  2. 可扩展性:增加新的命令和接收者非常容易,只需创建新的具体命令类,无需修改现有代码,符合开闭原则。
  3. 请求可被操作:将请求封装成对象后,可以像操作其他对象一样操作它,比如将其存入队列、日志、传递等。
  4. 支持高级功能:很容易实现撤销/重做、宏命令(将多个命令组合成一个复合命令)、事务等复杂功能。

缺点 (Cons):

  1. 类数量增加:对于每一个操作,都可能需要创建一个具体的命令类,这会导致系统中类的数量增多。
    • 优化:对于简单的命令,可以使用 Lambda 表达式 来代替具体的命令类,极大地简化代码。在 Java 8+ 中,() -> light.on() 就是一个天然的命令对象。

七、在 Java 中的应用

  • java.lang.Runnable: Runnable 接口是命令模式的完美体现。
    • Command: Runnable 接口,只有一个 run() 方法(相当于 execute())。
    • Concrete Command: 任何实现了 Runnable 的类,或者一个 Lambda 表达式。
    • Receiver: run() 方法内部调用的任何对象。
    • Invoker: Thread 类。new Thread(myRunnable).start() 就是调用者在执行命令。
  • Java Swing / AWT 中的 ActionListener:
    • Command: ActionListener 接口,只有一个 actionPerformed() 方法。
    • Invoker: JButton 等组件。
    • 当你调用 button.addActionListener(...) 时,就是在设置命令。
  • 数据库事务:可以将一系列数据库操作(Insert, Update, Delete)封装成命令对象,放入一个队列中。然后可以按顺序执行它们,如果中途出错,可以反向调用每个已执行命令的 undo() 方法,实现事务回滚。