系列定位:软考软件设计师 / 系统架构设计师 --- 行为型模式专题第 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 模式定义
命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
核心意图 :当一个系统需要 向某个对象发出请求 ,但 不希望调用者直接依赖接收者(如调用者不知道接收者是谁、接收者可能在远程、请求需要延迟执行或排队执行),如果调用者直接调用接收者的方法,会导致:
-
调用者与接收者紧耦合,新增接收者需要修改调用者
-
请求无法被记录、排队、撤销
-
无法将请求作为参数传递(如菜单项绑定不同的操作)
命令模式将 请求 封装成一个 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 代码实现
java
// 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 代码实现
java
// 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 代码实现
java
// 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();
}
}
六、三种场景对比与演进思路
| 维度 | 场景一:遥控器 | 场景二:事务管理 | 场景三:编辑器宏录制 |
|---|---|---|---|
| Command | Command(通用命令) |
TransactionCommand(事务命令) |
EditCommand(编辑命令) |
| ConcreteCommand | LightOnCommand、LightOffCommand、TVOnCommand |
InsertCommand、DeleteCommand |
InsertTextCommand、DeleteTextCommand |
| Invoker | RemoteControl(遥控器) |
TransactionManager(事务管理器) |
TextEditor(编辑器) |
| Receiver | Light、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)。
java
// 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)。
java
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:
- ❌ 错误:
java
class RemoteControl {
private Light light; // Invoker 直接持有 Receiver!
public void pressButton() {
light.on(); // 直接调用 Receiver,没有命令对象
}
}
- ✅ 正确:
java
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() 必须通过 反向操作 实现,而不是简单地"不做任何事":
- ❌ 错误:
java
public void undo() {
// 空实现!
}
- ✅ 正确:
java
public void undo() {
light.off(); // 开灯的撤销是关灯
}
软考中,如果 ConcreteCommand 提供了 undo() 方法,必须实现与 execute() 相反的逻辑。
陷阱 6:宏命令的 undo 顺序错误
宏命令(组合多个命令)的撤销必须 反向 遍历:
- ❌ 错误:
java
public void undo() {
for (Command cmd : commands) { // 正向撤销!顺序错了
cmd.undo();
}
}
- ✅ 正确:
java
public void undo() {
for (int i = commands.size() - 1; i >= 0; i--) { // 反向撤销
commands.get(i).undo();
}
}
如果正向撤销,先执行的命令后撤销,可能会因为后续命令已经改变了状态而导致撤销逻辑出错。
陷阱 7:Receiver 缺失
有些同学只写了 Invoker 和 Command,但忘了 Receiver。命令模式中,Command 是 对 Receiver 的封装,必须有 Receiver 存在:
- ❌ 错误:
java
class LightOnCommand implements Command {
// 没有 Receiver!
public void execute() {
System.out.println("开灯"); // 命令自己实现了逻辑
}
}
- ✅ 正确:
java
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 种。下一篇将总结设计模式到底在"守"什么原则?用代码把六大原则"钉"在每一行上。咱们下回见。