Command 模式,又称命令模式,它将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。这种模式将发出请求的对象和执行请求的对象解耦,使系统更加灵活,易于扩展。
基本结构
参与者
在 Command 模式中,我们可以抽象出以下参与者:
-
Command(命令)
声明执行操作的接口,通常只包含一个执行命令方法。
-
ConcreteCommand(具体命令)
定义一个接收者和行为的绑定关系,实现某个执行命令方法时会调用接收者的相应操作。
-
Invoker(调用者)
负责要求命令对象执行请求,不直接与接收者交互。
-
Receiver(接收者)
知道如何实施与执行一个请求相关的操作,任何类都可能作为一个接收者。
-
Client(客户)
接收并创建具体的命令对象并设定它的接收者。
类图结构
Client
: 客户端,创建具体的command
对象,并指定它的Receiver
。Client
负责组装command
与Receiver
。Command
: 命令接口,定义执行命令的方法。通常包含一个execute()
方法,有时(子接口)也会包含undo()
方法用于支持撤销操作。ConcreteCommand
: 具体命令类,实现Command
接口。它将一个Receiver
对象与一个动作绑定,调用Receiver
相应的action()
来实现execute()
方法。Invoker
: 调用者,负责调用command
对象执行请求。可以持有多个command
对象,并按照一定的规则(如队列、栈等)来执行这些command
。Receiver
: 接收者,知道如何执行与请求相关的操作,即action()
。action()
具体实现了command
要执行的动作。
实例演示
问题
编写一个基于命令行的字符串编辑器,支持字符串的追加、插入、删除等操作。编辑操作需要支持撤销和恢复。 命令如下:
- 追加:
a 'string'
- 插入:
i pos 'string'
- 删除:
d pos len
- 显示当前内容:
l
- 撤销:
undo
- 恢复:
redo
解决方案 1(不使用命令模式)
直接在编辑器类中处理所有字符串操作逻辑。这种方法的缺点是:
- 编辑器类会变得非常庞大和复杂
- 每增加一种操作,都需要修改编辑器类
- 很难实现撤销/重做功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StringEditor {
private StringBuilder content = new StringBuilder();
public void append(String str) {
content.append(str);
}
public void insert(int position, String str) {
content.insert(position, str);
}
public void delete(int position, int length) {
content.delete(position, position + length);
}
public String getContent() {
return content.toString();
}
// 实现撤销和重做功能会变得非常复杂
// 需要手动记录每个操作和状态...
}
解决方案 2(使用命令模式)
以下代码省略了构造函数以及部分基本函数
定义命令接口
1
2
3
4
5
6
7
interface Command {
void execute();
}
interface CanUndoCommand extends Command{
void undo();
}
定义接收者类
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
// Receiver
public class StringBuf {
private StringBuffer str;
public void append(String str) {
this.str.append(str);
}
// 返回删去的字符串部分,便于后续撤销和恢复
public String delete(int start, int end) {
int _end = end > this.str.length() ? this.str.length() : end;
String result = this.str.substring(start, _end);
this.str.delete(start, _end).toString();
return result;
}
public void insert(String str, int index) {
if (index < 0) {
index = 0;
} else if (index > this.str.length()) {
index = this.str.length();
}
this.str.insert(index, str);
}
}
实现具体命令类
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
// ConcreteCommand
class AppendCommand implements CanUndoCommand {
private StringBuffer buffer;
private String text;
@Override
public void execute() {
buffer.append(text);
}
@Override
public void undo() {
String content = buffer.getContent();
if (content.endsWith(text)) {
buffer.delete(content.length() - text.length(), text.length());
}
}
}
class InsertCommand implements CanUndoCommand {
private StringBuffer buffer;
private int position;
private String text;
@Override
public void execute() {
buffer.insert(position, text);
}
@Override
public void undo() {
buffer.delete(position, text.length());
}
}
class DeleteCommand implements CanUndoCommand {
private StringBuffer buffer;
private int position;
private int length;
private String deletedText;
@Override
public void execute() {
String content = buffer.getContent();
deletedText = content.substring(position, position + length);
buffer.delete(position, length);
}
@Override
public void undo() {
buffer.insert(position, deletedText);
}
}
class ShowCommand implements Command{
private StringBuffer str;
@Override
public void execute() {
System.out.println(str);
}
}
class UndoCommand implements Command{
private EditorInvoker editor;
@Override
public void execute() {
editor.undoLast()
}
}
class RedoCommand implements Command{
private EditorInvoker editor;
@Override
public void execute() {
editor.redoLast()
}
}
class
实现调用者类
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
// Invoker
class EditorInvoker {
private final Stack<Command> executedCommands = new Stack<>();
private final Stack<Command> undoneCommands = new Stack<>();
public void executeCommand(Command command) {
command.execute();
if(command instanceof CanUndoCommand){
executedCommands.push(command);
undoneCommands.clear(); // 若有新命令执行,则清空 redo 栈
}
}
public void undoLast() {
if (!executedCommands.isEmpty()) {
Command command = executedCommands.pop();
command.undo();
undoneCommands.push(command); // 将撤销的命令加入 redo 栈,便于恢复
}
}
public void redoLast() {
if (!undoneCommands.isEmpty()) {
Command command = undoneCommands.pop();
command.execute();
executedCommands.push(command);
}
}
}
客户端使用代码
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
// Client
public class Console {
public Command commandParser(String read){
// 通过 newCommand 分析得出命令类型
// 返回已经传入参数的 ConcreteCommand 对象(实现 Command 接口)
}
public static void main(String[] args) {
// 定义调用者和接受者
StringBuffer buffer = new StringBuffer();
EditorInvoker editor = new EditorInvoker();
Scanner scanner = new Scanner(System.in);
boolean running = true;
while (running) {
String read = Scanner.nextLine();
try{
Command command = commandParser(read);
editor.executeCommand(command);
} catch(Exception e){
System.out.println(e.getMessage())
}
}
scanner.close();
}
}
优势分析
本例中命令模式实现的主要优势:
-
松耦合:调用者与接收者完全解耦,调用者无需知道接收者的具体实现
- 扩展性强:
- 可以轻松添加新的命令而不修改现有代码
- 可以轻松添加新的接收者而不影响现有命令
-
支持撤销操作
-
命令组合:可以创建宏命令,将多个命令组合在一起执行
- 队列请求:命令对象可以存储在队列中,实现请求的延迟执行或日志记录
局限性分析
- 类爆炸:
- 每个具体命令都需要一个单独的类,可能导致类数量剧增
- 对于简单系统可能显得过于复杂
- 复杂状态管理:
- 复杂的撤销/重做功能需要保存更多的状态信息
- 性能考量:
- 增加了系统的间接层次,可能带来轻微性能损失
- 命令对象可能消耗额外内存
- 难以调试:
- 命令的执行流程更分散,可能增加调试难度
- 错误追踪需要跨越多个类
总结
命令模式是一种强大的行为型设计模式,它通过将请求封装为对象,实现了调用者与接收者的解耦。这种分离使系统更加灵活,可以独立地扩展和修改命令的执行方式和接收者的实现。命令模式特别适合需要队列执行、请求日志、撤销功能或事务操作的场景。在面对复杂交互系统时,命令模式能够提供清晰的结构和良好的扩展性,但对于简单系统可能会带来不必要的复杂度。在实际应用中,应当根据具体需求权衡是否使用这种模式。