系列定位:软考软件设计师 / 系统架构设计师 — 行为型模式专题第 11 讲

考察分值:上午题 1-2 分,下午题常作为代码填空或类图识别出现

难度等级:⭐⭐⭐☆☆(结构清晰,Command 接口与 Invoker/Receiver 的三角关系是填空重点)


一、考纲定位与模式定义
1.1 考纲要求

命令模式在软考中属于 行为型模式 的基础内容。考察形式包括:

  • 上午选择题:判断场景描述所属模式;识别命令模式与策略模式、备忘录模式的区别;判断 Command、ConcreteCommand、Invoker、Receiver、Client 五个角色;识别 "将请求封装为对象"、"解耦调用者与接收者" 的设计意图

  • 下午设计题:补全 Command 接口的 execute() 方法;补全 ConcreteCommand 中持有 Receiver 引用并在 execute() 中调用 Receiver 方法的代码;补全 Invoker 中设置命令和调用命令的代码;补全支持 undo() 的 Command 接口和具体实现;识别宏命令(MacroCommand)的组合结构

1.2 模式定义

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

核心意图:当一个系统需要 向某个对象发出请求,但 不希望调用者直接依赖接收者(如调用者不知道接收者是谁、接收者可能在远程、请求需要延迟执行或排队执行),如果调用者直接调用接收者的方法,会导致:

  1. 调用者与接收者紧耦合,新增接收者需要修改调用者

  2. 请求无法被记录、排队、撤销

  3. 无法将请求作为参数传递(如菜单项绑定不同的操作)

命令模式将 请求 封装成一个 Command 对象,Command 对象内部持有 Receiver 的引用,知道该调用 Receiver 的哪个方法。Invoker(调用者)只面向 Command 接口调用 execute(),完全不需要知道 Receiver 的存在。

通俗理解:

你去餐厅吃饭,服务员(Invoker)负责接单,但服务员并不直接炒菜。你点了一份宫保鸡丁(ConcreteCommand),服务员把订单(Command)递给后厨(Receiver),厨师看了订单才知道该做什么菜。订单在这里就是一个"命令对象":它封装了"让厨师做宫保鸡丁"这个请求。

如果没有命令模式,你得直接冲进厨房对厨师喊"炒个宫保鸡丁"——这就是调用者与接收者的紧耦合。有了命令模式,订单可以被排队(先炒鱼香肉丝再炒宫保鸡丁)、可以被取消(还没做的话退单)、甚至可以被记录(统计今天卖了多少份宫保鸡丁)。服务员不需要认识厨师,只需要会传订单。


二、UML 类图与角色划分
┌─────────────────────────────────────────────────────────┐
│                      Client                             │
│  + main()                                               │
│    ├── Receiver light = new Light()  // 接收者           │
│    ├── Command onCmd = new LightOnCommand(light)        │
│    ├── Command offCmd = new LightOffCommand(light)      │
│    ├── Invoker remote = new RemoteControl()             │
│    ├── remote.setCommand(onCmd)                         │
│    ├── remote.pressButton()  // 实际执行 light.on()      │
│    ├── remote.setCommand(offCmd)                        │
│    └── remote.pressButton()  // 实际执行 light.off()     │
└───────────────────────┬─────────────────────────────────┘
                        │ uses
                        ▼
┌─────────────────────────────────────────────────────────┐
│                  Invoker                                │
│  ── 调用者:触发命令,不知道 Receiver 的存在 ──              │
│  - command: Command   ◄── 组合关系                       │
│  + setCommand(Command c) { this.command = c; }          │
│  + pressButton() {                                      │
│      command.execute();  // 只调用命令,不直接操作灯        │
│    }                                                    │
│  + pressUndo() {                                        │
│      command.undo();     // 可选:支持撤销                │
│    }                                                    │
└───────────────────────┬─────────────────────────────────┘
                        │ delegates to
                        ▼
┌─────────────────────────────────────────────────────────┐
│                  <<interface>>                          │
│                    Command                              │
│  + execute()                                            │
│  + undo()     // 可选                                    │
└───────────────────────┬─────────────────────────────────┘
                        │ implements
                        ▼
┌─────────────────────────────────────────────────────────┐
│              ConcreteCommand                            │
│  ── 具体命令:封装 Receiver 和调用参数 ──                   │
│  - receiver: Receiver   ◄── 持有接收者引用                │
│  + execute() {                                          │
│      receiver.action();  // 实际调用接收者的方法           │
│    }                                                    │
│  + undo() {                                             │
│      receiver.reverseAction();  // 可选:反向操作         │
│    }                                                    │
└───────────────────────┬─────────────────────────────────┘
                        │ knows
                        ▼
┌─────────────────────────────────────────────────────────┐
│                  Receiver                               │
│  ── 接收者:知道如何执行具体操作 ──                          │
│  + action()                                             │
│  + reverseAction()                                      │
└─────────────────────────────────────────────────────────┘
角色职责软考填空关键词
Command(抽象命令)声明执行操作的接口(execute()),可声明撤销接口(undo())。所有命令对象都实现该接口interface Command + void execute() + void undo()
ConcreteCommand(具体命令)实现 Command 接口,持有 Receiver 的引用,在 execute() 中调用 Receiver 的具体方法。将调用者(Invoker)和接收者(Receiver)绑定在一起implements Command + private Receiver receiver + execute() 中调用 receiver.action()
Invoker(调用者)持有 Command 的引用,通过调用 command.execute() 来发起请求。Invoker 不需要知道 Receiver 的存在,只面向 Command 接口编程private Command command + setCommand() + pressButton() 调用 execute()
Receiver(接收者)知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者,它是请求的真正执行者具体的业务类,如 Light、Document、Database
Client(客户端)创建 ConcreteCommand 对象并设置其接收者,将 Command 对象交给 Invoker,触发 Invoker 执行new ConcreteCommand(receiver) + invoker.setCommand(command) + invoker.pressButton()

类图识别要点:软考类图中,命令模式的核心特征是 Invoker 组合 Command(聚合关系),ConcreteCommand 关联 Receiver(持有引用),且 ConcreteCommand 的 execute() 内部调用 receiver.action()。如果类图里有 "一个类持有命令对象,命令对象再调用另一个类的方法" 的三角关系 → 命令模式。


三、场景一:智能家居遥控器(最经典的命令模式)

业务背景:智能家居遥控器(Invoker)有多个按钮,每个按钮可以绑定不同的设备操作:开灯、关灯、开电视、关电视。遥控器不需要知道具体是灯还是电视,只需要绑定对应的命令对象。要求新增设备时不需要修改遥控器代码,且支持按钮的撤销操作。

说明:这是命令模式最经典的例子,也是软考下午题最爱考的 "请求封装" 场景。

3.1 代码实现
// Receiver:灯
public class Light {
    private String location;

    public Light(String location) {
        this.location = location;
    }

    public void on() {
        System.out.println("【灯】" + location + " 灯已打开");
    }

    public void off() {
        System.out.println("【灯】" + location + " 灯已关闭");
    }
}

// Receiver:电视
public class TV {
    public void on() {
        System.out.println("【电视】电视已打开");
    }

    public void off() {
        System.out.println("【电视】电视已关闭");
    }
}

// Command:命令接口
public interface Command {
    void execute();
    void undo();
}

// ConcreteCommand:开灯命令
public 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();  // 撤销:反向操作
    }
}

// ConcreteCommand:关灯命令
public 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();
    }
}

// ConcreteCommand:开电视命令
public class TVOnCommand implements Command {
    private TV tv;

    public TVOnCommand(TV tv) {
        this.tv = tv;
    }

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

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

// Invoker:遥控器
public class RemoteControl {
    private Command command;  // 当前绑定的命令
    private Command lastCommand;  // 记录上一个命令,用于撤销

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        System.out.println("【遥控器】按下按钮...");
        command.execute();
        lastCommand = command;  // 记录命令用于撤销
    }

    public void pressUndo() {
        System.out.println("【遥控器】按下撤销按钮...");
        if (lastCommand != null) {
            lastCommand.undo();
        }
    }
}

// Client:客户端
public class Client {
    public static void main(String[] args) {
        // 创建接收者
        Light livingRoomLight = new Light("客厅");
        TV tv = new TV();

        // 创建命令,绑定接收者
        Command lightOn = new LightOnCommand(livingRoomLight);
        Command lightOff = new LightOffCommand(livingRoomLight);
        Command tvOn = new TVOnCommand(tv);

        // 创建调用者(遥控器)
        RemoteControl remote = new RemoteControl();

        // 绑定并执行开灯
        remote.setCommand(lightOn);
        remote.pressButton();

        // 绑定并执行关灯
        remote.setCommand(lightOff);
        remote.pressButton();

        // 撤销:应该恢复开灯状态
        remote.pressUndo();

        System.out.println("---");

        // 绑定并执行开电视
        remote.setCommand(tvOn);
        remote.pressButton();
    }
}

关键体会:

  • RemoteControl(Invoker)只持有 Command 接口,不知道 Light 或 TV 的存在。新增一个设备(如空调),只需要新增 Command 和 Receiver,不需要修改 RemoteControl 的代码。这符合 开闭原则。

  • LightOnCommand(ConcreteCommand)是请求的真正封装者:它知道该调用哪个接收者(Light)的哪个方法(on())。遥控器只是"按按钮",命令对象才是"知道该干什么"的。

  • undo() 方法通过 反向操作 实现撤销(开灯的撤销是关灯)。这是命令模式支持撤销的基本方式。

  • 命令模式让请求变成了 一等对象(可以像普通对象一样被传递、存储、排队)。


四、场景二:数据库事务管理(实际开发场景)

业务背景:数据库操作需要封装为事务,支持提交(commit)和回滚(rollback)。每个操作(插入、删除、更新)都是一个命令,事务管理器(Invoker)负责按顺序执行这些命令,如果出错则统一回滚。要求操作可以被记录、排队、批量执行。

说明:这是实际开发中命令模式的典型应用(JDBC 的 Statement、JPA 的 EntityTransaction 均使用类似设计),也是软考下午题中 "事务封装" 场景的代表。

4.1 代码实现
// Receiver:数据库连接
public class Database {
    private Map<String, String> data = new HashMap<>();

    public void insert(String key, String value) {
        data.put(key, value);
        System.out.println("【数据库】插入: " + key + " = " + value);
    }

    public void delete(String key) {
        data.remove(key);
        System.out.println("【数据库】删除: " + key);
    }

    public void update(String key, String value) {
        data.put(key, value);
        System.out.println("【数据库】更新: " + key + " = " + value);
    }

    public void display() {
        System.out.println("【数据库】当前数据: " + data);
    }
}

// Command:事务命令接口
public interface TransactionCommand {
    void execute();
    void undo();
}

// ConcreteCommand:插入命令
public class InsertCommand implements TransactionCommand {
    private Database db;
    private String key;
    private String value;

    public InsertCommand(Database db, String key, String value) {
        this.db = db;
        this.key = key;
        this.value = value;
    }

    @Override
    public void execute() {
        db.insert(key, value);
    }

    @Override
    public void undo() {
        db.delete(key);  // 撤销插入 = 删除
    }
}

// ConcreteCommand:删除命令
public class DeleteCommand implements TransactionCommand {
    private Database db;
    private String key;
    private String oldValue;  // 记录旧值用于回滚

    public DeleteCommand(Database db, String key) {
        this.db = db;
        this.key = key;
        this.oldValue = null;  // 实际应从数据库获取
    }

    @Override
    public void execute() {
        // 实际应先保存旧值
        db.delete(key);
    }

    @Override
    public void undo() {
        if (oldValue != null) {
            db.insert(key, oldValue);  // 撤销删除 = 恢复插入
        }
    }
}

// Invoker:事务管理器
public class TransactionManager {
    private List<TransactionCommand> commands = new ArrayList<>();
    private List<TransactionCommand> history = new ArrayList<>();

    public void addCommand(TransactionCommand command) {
        commands.add(command);
    }

    public void commit() {
        System.out.println("【事务管理器】开始执行事务...");
        try {
            for (TransactionCommand cmd : commands) {
                cmd.execute();
                history.add(cmd);  // 记录已执行的命令
            }
            System.out.println("【事务管理器】事务提交成功");
            commands.clear();
        } catch (Exception e) {
            System.out.println("【事务管理器】执行失败,开始回滚...");
            rollback();
        }
    }

    public void rollback() {
        // 反向撤销已执行的命令
        for (int i = history.size() - 1; i >= 0; i--) {
            history.get(i).undo();
        }
        history.clear();
        commands.clear();
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        Database db = new Database();
        TransactionManager tx = new TransactionManager();

        // 添加事务命令
        tx.addCommand(new InsertCommand(db, "user1", "Alice"));
        tx.addCommand(new InsertCommand(db, "user2", "Bob"));
        tx.addCommand(new InsertCommand(db, "user3", "Charlie"));

        // 执行事务
        tx.commit();
        db.display();

        System.out.println("---");

        // 模拟回滚
        tx.rollback();
        db.display();
    }
}

五、场景三:文本编辑器宏录制(宏命令与撤销重做场景)

业务背景:文本编辑器需要支持宏录制(记录一系列操作并批量重放)和撤销重做(Undo/Redo)。每个编辑操作(输入、删除、替换)都是一个命令,编辑器(Invoker)维护命令历史栈。支持 Ctrl+Z 撤销、Ctrl+Y 重做、宏录制批量执行。使用命令模式将操作封装为对象。

说明:这是命令模式在宏命令与撤销重做场景中的典型应用,也是软考上午题中 "命令队列" 场景的代表。

5.1 代码实现
// Receiver:文档
public class Document {
    private StringBuilder content = new StringBuilder();

    public void insert(String text, int position) {
        content.insert(position, text);
    }

    public void delete(int start, int length) {
        content.delete(start, start + length);
    }

    public String getContent() {
        return content.toString();
    }

    public int length() {
        return content.length();
    }
}

// Command:编辑命令接口
public interface EditCommand {
    void execute();
    void undo();
}

// ConcreteCommand:插入文本
public class InsertTextCommand implements EditCommand {
    private Document doc;
    private String text;
    private int position;

    public InsertTextCommand(Document doc, String text, int position) {
        this.doc = doc;
        this.text = text;
        this.position = position;
    }

    @Override
    public void execute() {
        doc.insert(text, position);
    }

    @Override
    public void undo() {
        doc.delete(position, text.length());
    }
}

// ConcreteCommand:删除文本
public class DeleteTextCommand implements EditCommand {
    private Document doc;
    private String deletedText;
    private int position;

    public DeleteTextCommand(Document doc, int start, int length) {
        this.doc = doc;
        this.position = start;
        this.deletedText = doc.getContent().substring(start, start + length);
    }

    @Override
    public void execute() {
        doc.delete(position, deletedText.length());
    }

    @Override
    public void undo() {
        doc.insert(deletedText, position);
    }
}

// Invoker:编辑器
public class TextEditor {
    private Document doc = new Document();
    private Stack<EditCommand> undoStack = new Stack<>();
    private Stack<EditCommand> redoStack = new Stack<>();

    public void executeCommand(EditCommand command) {
        command.execute();
        undoStack.push(command);
        redoStack.clear();  // 新操作后清空重做栈
    }

    public void undo() {
        if (!undoStack.isEmpty()) {
            EditCommand command = undoStack.pop();
            command.undo();
            redoStack.push(command);
            System.out.println("【编辑器】撤销操作");
        }
    }

    public void redo() {
        if (!redoStack.isEmpty()) {
            EditCommand command = redoStack.pop();
            command.execute();
            undoStack.push(command);
            System.out.println("【编辑器】重做操作");
        }
    }

    public void show() {
        System.out.println("【文档内容】\"" + doc.getContent() + "\"");
    }
}

// MacroCommand:宏命令(组合命令)
public class MacroCommand implements EditCommand {
    private List<EditCommand> commands = new ArrayList<>();

    public void add(EditCommand command) {
        commands.add(command);
    }

    @Override
    public void execute() {
        for (EditCommand cmd : commands) {
            cmd.execute();
        }
    }

    @Override
    public void undo() {
        // 反向撤销
        for (int i = commands.size() - 1; i >= 0; i--) {
            commands.get(i).undo();
        }
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        Document doc = new Document();

        // 执行插入
        editor.executeCommand(new InsertTextCommand(doc, "Hello", 0));
        editor.executeCommand(new InsertTextCommand(doc, " World", 5));
        editor.show();

        // 撤销
        editor.undo();
        editor.show();

        // 重做
        editor.redo();
        editor.show();

        System.out.println("---");

        // 宏录制:批量插入
        MacroCommand macro = new MacroCommand();
        macro.add(new InsertTextCommand(doc, "!", doc.length()));
        macro.add(new InsertTextCommand(doc, "!", doc.length()));
        macro.add(new InsertTextCommand(doc, "!", doc.length()));

        editor.executeCommand(macro);
        editor.show();
    }
}

六、三种场景对比与演进思路
维度场景一:遥控器场景二:事务管理场景三:编辑器宏录制
CommandCommand(通用命令)TransactionCommand(事务命令)EditCommand(编辑命令)
ConcreteCommandLightOnCommand、LightOffCommand、TVOnCommandInsertCommand、DeleteCommandInsertTextCommand、DeleteTextCommand
InvokerRemoteControl(遥控器)TransactionManager(事务管理器)TextEditor(编辑器)
ReceiverLight、TV(家电设备)Database(数据库)Document(文档)
undo 实现反向操作(开↔关)反向操作(插入↔删除)反向操作(插入↔删除)
批量/队列单命令执行命令列表批量提交宏命令(MacroCommand)组合多个命令
历史记录lastCommand(单命令)history(已执行列表)undoStack + redoStack(双栈)
新增扩展新增设备 + 新增命令类新增操作类型 + 新增命令类新增编辑操作 + 新增命令类
软考考法下午大题(补全命令封装)下午大题(补全事务回滚)上午选择题(识别宏命令)

七、命令模式 vs 策略模式 vs 备忘录模式:核心对比
对比项命令模式策略模式备忘录模式
模式分类行为型行为型行为型
核心思想封装请求 为对象,解耦调用者与接收者封装 算法族,让它们可以相互替换捕获并保存 对象状态,支持恢复
封装内容请求(调用谁、什么方法、什么参数)算法(不同的实现方式)状态(对象内部数据的快照)
关注点谁发起请求 与 谁执行请求 的解耦怎么做 的灵活替换曾经是什么状态 的保存与恢复
undo 方式通过 反向操作(如 undo() 方法)实现撤销不支持 撤销通过 恢复状态快照 实现撤销
角色关系Invoker → Command → Receiver(三角关系)Context → Strategy(组合关系)Originator ↔ Memento,Caretaker 保管
请求对象化是(请求是一等对象,可排队、记录、传递)否(策略是算法的替换)否(备忘录是状态的快照)
典型应用遥控器、事务管理、撤销重做、宏命令促销策略、支付方式、排序算法编辑器撤销、游戏存档、事务回滚
软考判断看到 "请求封装为对象"、"调用者与接收者解耦"、"命令队列"、"undo" → 命令看到 "多种算法可替换"、"客户端选择" → 策略看到 "保存状态恢复"、"快照"、"不破坏封装" → 备忘录

记忆口诀:命令是 "传令兵"(把请求包成对象到处传),策略是 "工具箱"(多种算法挑一个用),备忘录是 "存档点"(保存状态随时读档)。


八、软考高频考点与易混淆辨析
8.1 高频考点
考点内容
模式分类行为型模式(GoF 23 正式成员)
核心五角色Command(抽象命令)、ConcreteCommand(具体命令)、Invoker(调用者)、Receiver(接收者)、Client(客户端)
核心思想将请求封装为对象,解耦调用者(Invoker)与接收者(Receiver),支持请求的排队、记录、撤销
三角关系Invoker 组合 Command,ConcreteCommand 关联 Receiver,形成调用三角
execute 方法ConcreteCommand 的 execute() 内部调用 receiver.action()
undo 方法通过反向操作实现撤销(如开灯的 undo 是关灯),不是恢复状态
宏命令MacroCommand 组合多个 Command,实现批量执行
与策略模式区别命令:封装 请求(调用谁、什么方法);策略:封装 算法(不同实现方式)
与备忘录模式区别命令:undo 通过 反向操作;备忘录:undo 通过 恢复状态快照
适用场景① 需要解耦调用者和接收者 ② 请求需要排队、记录、撤销 ③ 需要支持宏命令
不适用场景请求非常简单,直接调用即可;不需要撤销、排队等高级功能
8.2 易混淆辨析:命令 vs 策略 vs 备忘录
对比项命令模式策略模式备忘录模式
UML 结构Invoker → Command → Receiver(三角)Context → Strategy(组合)Originator ↔ Memento,Caretaker 保管
封装对象请求(谁调用谁、什么参数)算法(不同实现)状态(数据快照)
undo 机制反向操作(如 undo() 方法)无状态恢复(restoreMemento)
请求对象化是(请求是一等对象)否否
软考考法代码填空(补全 execute + receiver.action + undo)代码填空(补全 setStrategy + execute)代码填空(补全 createMemento + restoreMemento)

九、真题风格模拟与代码填空
模拟题 1(上午选择题)

以下关于命令模式的叙述中,正确的是()。

A. 命令模式属于结构型模式,主要用于统一叶子和组合的接口

B. 在命令模式中,Invoker 必须直接持有 Receiver 的引用并调用其方法

C. 命令模式将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化,并支持可撤销的操作

D. 命令模式与策略模式的核心区别在于:命令模式封装算法族,策略模式封装请求

答案:C

解析:

  • A 错误:命令模式属于 行为型 模式,不是结构型。统一叶子和组合接口的是 组合模式。

  • B 错误:Invoker 不需要也不应该 直接持有 Receiver 的引用。Invoker 只面向 Command 接口调用 execute(),Command 内部才持有 Receiver 并调用其方法。这是解耦的核心。

  • C 正确:这是命令模式的核心定义(GoF 原书定义)。

  • D 错误:说反了。命令模式封装的是 请求,策略模式封装的是 算法族。


模拟题 2(下午代码填空 — 补全命令封装与调用)

某系统使用命令模式实现文件操作。 FileReceiver 是接收者, FileCommand 是命令接口。请补全(1)~(4)。

// Receiver
class FileManager {
    public void copy(String src, String dest) {
        System.out.println("复制文件: " + src + " -> " + dest);
    }

    public void delete(String path) {
        System.out.println("删除文件: " + path);
    }
}

// Command
interface FileCommand {
    void execute();
    void undo();
}

// ConcreteCommand:复制命令
class CopyCommand implements FileCommand {
    private FileManager receiver;
    private String src;
    private String dest;

    public CopyCommand(FileManager receiver, String src, String dest) {
        this.receiver = receiver;
        this.src = src;
        this.dest = dest;
    }

    @Override
    public void execute() {
        (1)______;  // 调用接收者执行复制
    }

    @Override
    public void undo() {
        receiver.delete(dest);  // 撤销复制 = 删除目标文件
    }
}

// Invoker
class FileInvoker {
    private FileCommand command;

    public void setCommand(FileCommand command) {
        this.command = command;
    }

    public void invoke() {
        (2)______;  // 执行命令
    }

    public void undo() {
        if (command != null) {
            (3)______;  // 撤销命令
        }
    }
}

public class Client {
    public static void main(String[] args) {
        FileManager manager = new FileManager();
        FileInvoker invoker = new FileInvoker();

        // 创建复制命令
        FileCommand copyCmd = (4)______;
        invoker.setCommand(copyCmd);
        invoker.invoke();
    }
}

答案:

  • (1) receiver.copy(src, dest)

  • (2) command.execute()

  • (3) command.undo()

  • (4) new CopyCommand(manager, "a.txt", "b.txt")

阅卷要点:

  • (1) 必须是 receiver.copy(src, dest)。ConcreteCommand 的 execute() 必须调用 Receiver 的具体方法来完成请求。如果写 copy(src, dest) 或 executeCopy() 会 零分。

  • (2) 必须是 command.execute()。Invoker 通过调用 Command 接口的 execute() 方法来发起请求,而不是直接调用 Receiver。如果写 command.run() 或 receiver.copy() 会 零分。

  • (3) 必须是 command.undo()。Invoker 的撤销操作应该委托给 Command 的 undo() 方法。如果写 command.cancel() 或 receiver.delete() 会 零分。

  • (4) 必须是 new CopyCommand(manager, "a.txt", "b.txt")。Client 创建 ConcreteCommand 时需要传入 Receiver 引用和具体参数。如果写 new FileCommand() 会 零分(接口不能实例化)。


模拟题 3(下午代码填空 — 补全宏命令)

某系统使用命令模式实现宏命令。 MacroCommand 组合多个 Command 并批量执行。请补全(1)~(2)。

interface Command {
    void execute();
    void undo();
}

// MacroCommand:宏命令
class MacroCommand implements Command {
    private List<Command> commands = new ArrayList<>();

    public void add(Command command) {
        commands.add(command);
    }

    @Override
    public void execute() {
        for (Command cmd : commands) {
            (1)______;  // 批量执行
        }
    }

    @Override
    public void undo() {
        // 反向撤销
        for (int i = commands.size() - 1; i >= 0; i--) {
            (2)______;
        }
    }
}

答案:

  • (1) cmd.execute()

  • (2) commands.get(i).undo()

阅卷要点:

  • (1) 必须是 cmd.execute()。宏命令的 execute() 遍历所有子命令并逐个执行。如果写 cmd.run() 或 execute(cmd) 会 零分。

  • (2) 必须是 commands.get(i).undo()。宏命令的撤销需要 反向 遍历并撤销每个子命令。如果写 commands.get(i).execute() 会 零分(逻辑错误,撤销变成了执行)。


十、常见陷阱与注意事项
陷阱 1:误认为命令是结构型模式

命令模式属于 行为型 模式,因为它关注的是 对象之间的行为交互(请求的封装与传递),而不是类与类之间的结构关系。软考上午题如果问"以下属于结构型模式的是",选项里出现"命令" 不能选。

陷阱 2:Invoker 直接调用 Receiver

这是软考 最高频的失分点。命令模式的核心就是解耦 Invoker 与 Receiver:

  • ❌ 错误:

class RemoteControl {
    private Light light;  // Invoker 直接持有 Receiver!

    public void pressButton() {
        light.on();  // 直接调用 Receiver,没有命令对象
    }
}
  • ✅ 正确:

class RemoteControl {
    private Command command;  // Invoker 只持有 Command

    public void pressButton() {
        command.execute();  // 通过 Command 间接调用
    }
}

Invoker 绝对不能直接依赖 Receiver。如果 Invoker 里出现了 Light 或 TV 类型的字段,就违背了命令模式的设计意图。

陷阱 3:与策略模式混淆

这是软考 最难的辨析题 之一。两者的核心区别:

维度命令模式策略模式
封装内容请求(调用谁、什么方法、什么参数)算法(不同的实现方式)
角色关系Invoker → Command → Receiver(三角关系)Context → Strategy(组合关系)
关注点谁调用谁 的解耦怎么做 的灵活替换
请求对象化是(请求是一等对象,可排队、撤销)否(策略是算法的替换)
undo支持(通过反向操作)不支持
典型方法execute()、undo()algorithm()、execute()

终极区分方法:看封装的内容和目的。

  • 如果是为了 把一次具体的调用(谁、什么方法、什么参数)包成对象,支持撤销、排队、记录 → 命令。

  • 如果是为了 提供多种算法实现同一接口,让客户端选择 → 策略。

陷阱 4:与备忘录模式混淆
维度命令模式备忘录模式
undo 方式反向操作(如 undo() 中执行相反的方法)状态恢复(恢复之前保存的状态快照)
保存内容保存 操作/请求 本身保存 状态数据
典型应用遥控器撤销、事务回滚(通过反向 SQL)编辑器撤销、游戏存档(通过恢复数据)

如果类图里有 execute() 和 undo(),且 undo() 是通过反向操作(如 insert 的 undo 是 delete)实现的 → 命令模式。如果类图里有 createMemento() 和 restoreMemento(),且恢复是直接替换状态 → 备忘录模式。

陷阱 5:undo 实现不正确

命令模式的 undo() 必须通过 反向操作 实现,而不是简单地"不做任何事":

  • ❌ 错误:

public void undo() {
    // 空实现!
}
  • ✅ 正确:

public void undo() {
    light.off();  // 开灯的撤销是关灯
}

软考中,如果 ConcreteCommand 提供了 undo() 方法,必须实现与 execute() 相反的逻辑。

陷阱 6:宏命令的 undo 顺序错误

宏命令(组合多个命令)的撤销必须 反向 遍历:

  • ❌ 错误:

public void undo() {
    for (Command cmd : commands) {  // 正向撤销!顺序错了
        cmd.undo();
    }
}
  • ✅ 正确:

public void undo() {
    for (int i = commands.size() - 1; i >= 0; i--) {  // 反向撤销
        commands.get(i).undo();
    }
}

如果正向撤销,先执行的命令后撤销,可能会因为后续命令已经改变了状态而导致撤销逻辑出错。

陷阱 7:Receiver 缺失

有些同学只写了 Invoker 和 Command,但忘了 Receiver。命令模式中,Command 是 对 Receiver 的封装,必须有 Receiver 存在:

  • ❌ 错误:

class LightOnCommand implements Command {
    // 没有 Receiver!
    public void execute() {
        System.out.println("开灯");  // 命令自己实现了逻辑
    }
}
  • ✅ 正确:

class LightOnCommand implements Command {
    private Light light;  // 持有 Receiver
    
    public void execute() {
        light.on();  // 调用 Receiver 的方法
    }
}

软考中,ConcreteCommand 必须持有 Receiver 的引用,并在 execute() 中调用 Receiver 的方法。如果命令对象自己实现了所有逻辑,就退化成了策略模式。


十一、总结
要点内容
定义将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作
分类行为型模式(GoF 23 正式成员)
核心五角色Command(抽象命令)、ConcreteCommand(具体命令)、Invoker(调用者)、Receiver(接收者)、Client(客户端)
核心思想将请求封装为对象,解耦调用者(Invoker)与接收者(Receiver)
三角关系Invoker 组合 Command,ConcreteCommand 关联 Receiver
execute 方法ConcreteCommand 的 execute() 内部调用 receiver.action()
undo 方法通过反向操作实现撤销(如开灯的 undo 是关灯)
宏命令MacroCommand 组合多个 Command,批量执行,反向撤销
与策略模式区别命令:封装 请求(谁调用谁);策略:封装 算法(不同实现)
与备忘录模式区别命令:undo 通过 反向操作;备忘录:undo 通过 恢复状态快照
适用场景① 需要解耦调用者和接收者 ② 请求需要排队、记录、撤销 ③ 需要支持宏命令
不适用场景请求简单直接,不需要撤销、排队等高级功能
软考重点代码填空(补全 execute + receiver.action + undo + 宏命令);与策略、备忘录模式的辨析;Invoker 不直接依赖 Receiver
答题技巧看到 "请求封装为对象"、"调用者与接收者解耦"、"命令队列"、"undo"、"遥控器" → 命令;看到 Invoker 只持有 Command、Command 持有 Receiver → 确认命令模式

系列预告:至此,行为型模式已经讲解了策略、状态、模板方法、访问者、中介者、备忘录、解释器、迭代器、观察者、责任链、命令共 11 种。下一篇将总结设计模式到底在"守"什么原则?用代码把六大原则"钉"在每一行上。咱们下回见。

更多推荐