命令模式(Command)

当需要将请求封装为一个对象,使得可以用不同的请求对客户进行参数化时......

Posted by CloudingYu on April 14, 2025

Command 模式,又称命令模式,它将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。这种模式将发出请求的对象和执行请求的对象解耦,使系统更加灵活,易于扩展。

基本结构

参与者

在 Command 模式中,我们可以抽象出以下参与者:

  • Command(命令)

    声明执行操作的接口,通常只包含一个执行命令方法。

  • ConcreteCommand(具体命令)

    定义一个接收者和行为的绑定关系,实现某个执行命令方法时会调用接收者的相应操作。

  • Invoker(调用者)

    负责要求命令对象执行请求,不直接与接收者交互。

  • Receiver(接收者)

    知道如何实施与执行一个请求相关的操作,任何类都可能作为一个接收者。

  • Client(客户)

    接收并创建具体的命令对象并设定它的接收者。

类图结构

  • Client: 客户端,创建具体的 command 对象,并指定它的 ReceiverClient 负责组装 commandReceiver
  • 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();
    }
}

优势分析

本例中命令模式实现的主要优势:

  1. 松耦合:调用者与接收者完全解耦,调用者无需知道接收者的具体实现

  2. 扩展性强
    • 可以轻松添加新的命令而不修改现有代码
    • 可以轻松添加新的接收者而不影响现有命令
  3. 支持撤销操作

  4. 命令组合:可以创建宏命令,将多个命令组合在一起执行

  5. 队列请求:命令对象可以存储在队列中,实现请求的延迟执行或日志记录

局限性分析

  1. 类爆炸
    • 每个具体命令都需要一个单独的类,可能导致类数量剧增
    • 对于简单系统可能显得过于复杂
  2. 复杂状态管理
    • 复杂的撤销/重做功能需要保存更多的状态信息
  3. 性能考量
    • 增加了系统的间接层次,可能带来轻微性能损失
    • 命令对象可能消耗额外内存
  4. 难以调试
    • 命令的执行流程更分散,可能增加调试难度
    • 错误追踪需要跨越多个类

总结

命令模式是一种强大的行为型设计模式,它通过将请求封装为对象,实现了调用者与接收者的解耦。这种分离使系统更加灵活,可以独立地扩展和修改命令的执行方式和接收者的实现。命令模式特别适合需要队列执行、请求日志、撤销功能或事务操作的场景。在面对复杂交互系统时,命令模式能够提供清晰的结构和良好的扩展性,但对于简单系统可能会带来不必要的复杂度。在实际应用中,应当根据具体需求权衡是否使用这种模式。