Appearance
命令模式 (Command)
一、定义
一句话概括:将请求封装为对象,从而可以用不同的请求对客户进行参数化,支持请求排队、记录日志、撤销和重做等操作。
官方定义:Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
二、解决的问题
2.1 问题场景
在 GUI 开发中,按钮点击、菜单选择等操作通常通过回调函数实现。但是:
- 同一个按钮在不同场景下需要执行不同的操作
- 需要支持撤销(Undo)/ 重做(Redo)
- 需要将操作记录到日志中
- 需要将操作放入队列异步执行
如果直接在按钮的点击事件中写业务逻辑,会导致:
- 调用者与执行者紧密耦合
- 无法支持撤销操作
- 难以扩展新的操作
2.2 不用命令模式会怎样?
java
// 反例:按钮直接调用业务逻辑
class Button {
private TextEditor editor;
public void click() {
if (action.equals("copy")) {
editor.copy(); // 紧耦合
} else if (action.equals("cut")) {
editor.cut();
}
// 每增加一个操作都要修改 Button 类
}
}三、结构
3.1 角色组成
| 角色 | 说明 |
|---|---|
| Command(抽象命令) | 定义命令接口,通常包含 execute() 和 undo() 方法 |
| ConcreteCommand(具体命令) | 实现命令接口,绑定接收者与操作 |
| Receiver(接收者) | 实际执行操作的对象 |
| Invoker(调用者) | 发送命令的对象,持有命令的引用 |
| Client(客户端) | 创建具体命令对象并为其设置接收者 |
3.2 类图(ASCII)
┌──────────┐ ┌───────────────┐ ┌──────────┐
│ Client │────────►│ Command │◄────────│ Invoker │
└──────────┘ ├───────────────┤ └──────────┘
│ + execute() │
│ + undo() │
└───────┬───────┘
│
┌────────┴────────┐
│ │
▼ ▼
┌───────────────┐┌───────────────┐
│ConcreteCmd A ││ConcreteCmd B │
├───────────────┤├───────────────┤
│ - receiver ││ - receiver │
│ + execute() ││ + execute() │
│ + undo() ││ + undo() │
└───────┬───────┘└───────┬───────┘
│ │
▼ ▼
┌───────────────┐
│ Receiver │
│ + action() │
└───────────────┘四、代码实现
4.1 基础实现
java
// ==================== 接收者 ====================
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 + " 灯灭了");
}
}
// ==================== 抽象命令 ====================
interface Command {
void execute();
void undo();
}
// ==================== 具体命令 ====================
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();
}
}
// ==================== 调用者 ====================
class RemoteControl {
private Command[] onCommands;
private Command[] offCommands;
private Command undoCommand;
public RemoteControl(int slots) {
onCommands = new Command[slots];
offCommands = new Command[slots];
Command noCommand = new NoCommand();
for (int i = 0; i < slots; 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 pressOnButton(int slot) {
onCommands[slot].execute();
undoCommand = onCommands[slot];
}
public void pressOffButton(int slot) {
offCommands[slot].execute();
undoCommand = offCommands[slot];
}
public void pressUndo() {
undoCommand.undo();
}
}
// 空命令(Null Object 模式)
class NoCommand implements Command {
public void execute() { }
public void undo() { }
}
// ==================== 客户端 ====================
public class CommandDemo {
public static void main(String[] args) {
RemoteControl remote = new RemoteControl(3);
Light livingRoomLight = new Light("客厅");
Light kitchenLight = new Light("厨房");
remote.setCommand(0,
new LightOnCommand(livingRoomLight),
new LightOffCommand(livingRoomLight));
remote.setCommand(1,
new LightOnCommand(kitchenLight),
new LightOffCommand(kitchenLight));
remote.pressOnButton(0); // 客厅灯亮了
remote.pressOffButton(0); // 客厅灯灭了
remote.pressUndo(); // 客厅灯亮了(撤销)
remote.pressOnButton(1); // 厨房灯亮了
}
}4.2 进阶实现
4.2.1 命令队列
java
class CommandQueue {
private Queue<Command> queue = new LinkedList<>();
public void addCommand(Command command) {
queue.offer(command);
}
public void executeAll() {
while (!queue.isEmpty()) {
Command command = queue.poll();
command.execute();
}
}
}4.2.2 撤销操作与操作历史
java
import java.util.Stack;
class TextEditor {
private StringBuilder content = new StringBuilder();
public void append(String text) {
content.append(text);
}
public void delete(int length) {
content.delete(content.length() - length, content.length());
}
public String getContent() {
return content.toString();
}
}
class AppendCommand implements Command {
private TextEditor editor;
private String text;
public AppendCommand(TextEditor editor, String text) {
this.editor = editor;
this.text = text;
}
@Override
public void execute() {
editor.append(text);
}
@Override
public void undo() {
editor.delete(text.length());
}
}
class CommandHistory {
private Stack<Command> undoStack = new Stack<>();
private Stack<Command> redoStack = new Stack<>();
public void execute(Command command) {
command.execute();
undoStack.push(command);
redoStack.clear(); // 新操作后清空 redo 栈
}
public void undo() {
if (!undoStack.isEmpty()) {
Command command = undoStack.pop();
command.undo();
redoStack.push(command);
}
}
public void redo() {
if (!redoStack.isEmpty()) {
Command command = redoStack.pop();
command.execute();
undoStack.push(command);
}
}
}
// 使用示例
public class UndoRedoDemo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
CommandHistory history = new CommandHistory();
history.execute(new AppendCommand(editor, "Hello "));
System.out.println(editor.getContent()); // Hello
history.execute(new AppendCommand(editor, "World!"));
System.out.println(editor.getContent()); // Hello World!
history.undo();
System.out.println(editor.getContent()); // Hello
history.redo();
System.out.println(editor.getContent()); // Hello World!
}
}4.2.3 宏命令
java
class MacroCommand implements Command {
private List<Command> commands = new ArrayList<>();
public void addCommand(Command command) {
commands.add(command);
}
@Override
public void execute() {
for (Command command : commands) {
command.execute();
}
}
@Override
public void undo() {
// 逆序撤销
for (int i = commands.size() - 1; i >= 0; i--) {
commands.get(i).undo();
}
}
}4.3 生产级实现
Spring Boot 订单操作命令模式
java
// ==================== 订单 ====================
@Data
@AllArgsConstructor
class Order {
private Long id;
private String status; // CREATED, PAID, SHIPPED, COMPLETED, CANCELLED
}
// ==================== 订单服务(接收者) ====================
@Service
class OrderService {
public void pay(Long orderId) {
// 支付逻辑
System.out.println("订单 " + orderId + " 已支付");
}
public void ship(Long orderId) {
// 发货逻辑
System.out.println("订单 " + orderId + " 已发货");
}
public void cancel(Long orderId) {
// 取消逻辑
System.out.println("订单 " + orderId + " 已取消");
}
}
// ==================== 命令接口 ====================
interface OrderCommand {
void execute();
void undo();
String getType();
}
// ==================== 具体命令 ====================
class PayOrderCommand implements OrderCommand {
private final OrderService orderService;
private final Long orderId;
public PayOrderCommand(OrderService orderService, Long orderId) {
this.orderService = orderService;
this.orderId = orderId;
}
@Override
public void execute() { orderService.pay(orderId); }
@Override
public void undo() { orderService.cancel(orderId); }
@Override
public String getType() { return "PAY"; }
}
class ShipOrderCommand implements OrderCommand {
private final OrderService orderService;
private final Long orderId;
public ShipOrderCommand(OrderService orderService, Long orderId) {
this.orderService = orderService;
this.orderId = orderId;
}
@Override
public void execute() { orderService.ship(orderId); }
@Override
public void undo() {
System.out.println("发货已撤销,订单 " + orderId + " 回退到已支付");
}
@Override
public String getType() { return "SHIP"; }
}
// ==================== 命令调用者(异步执行) ====================
@Service
class OrderCommandInvoker {
private final BlockingQueue<OrderCommand> commandQueue = new LinkedBlockingQueue<>();
private final Stack<OrderCommand> history = new Stack<>();
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@PostConstruct
public void start() {
executor.submit(() -> {
while (true) {
try {
OrderCommand cmd = commandQueue.take();
cmd.execute();
history.push(cmd);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
}
public void submit(OrderCommand command) {
commandQueue.offer(command);
}
public void undoLast() {
if (!history.isEmpty()) {
history.pop().undo();
}
}
}
// ==================== Controller ====================
@RestController
@RequestMapping("/api/orders")
class OrderController {
@Autowired private OrderService orderService;
@Autowired private OrderCommandInvoker invoker;
@PostMapping("/{id}/pay")
public String pay(@PathVariable Long id) {
invoker.submit(new PayOrderCommand(orderService, id));
return "支付命令已提交";
}
@PostMapping("/{id}/ship")
public String ship(@PathVariable Long id) {
invoker.submit(new ShipOrderCommand(orderService, id));
return "发货命令已提交";
}
@PostMapping("/undo")
public String undo() {
invoker.undoLast();
return "已撤销最后一个操作";
}
}五、优缺点
优点
- 解耦调用者和接收者:调用者只知道命令接口,不需要知道具体实现
- 易于扩展:新增命令只需实现 Command 接口,符合开闭原则
- 支持撤销/重做:通过存储命令历史实现 Undo/Redo
- 支持命令队列:可以将命令排队、异步执行
- 支持组合命令:宏命令可以组合多个简单命令
缺点
- 类膨胀:每个具体操作都需要一个命令类,类数量会大量增加
- 复杂度增加:引入了多层抽象,增加了系统复杂度
- 内存开销:维护命令历史需要额外的内存空间
六、适用场景
- 需要撤销/重做功能:文本编辑器、IDE、图片编辑器
- 操作需要排队执行:消息队列、任务调度
- 操作需要记录日志:审计系统、操作日志记录
- GUI 按钮与菜单:按钮、菜单项与操作的解耦
- 事务操作:数据库事务的提交和回滚
- 分布式任务:将操作序列化后发送到远程执行
七、JDK / Spring 框架中的实际应用
| 框架 | 应用位置 | 说明 |
|---|---|---|
| JDK | java.lang.Runnable | 命令接口,Thread 是调用者 |
| JDK | javax.swing.Action | Swing 中的命令模式 |
| Spring | JdbcTemplate | PreparedStatementCallback 是命令 |
| Spring MVC | @RequestMapping | 方法级别的命令映射 |
| MyBatis | Executor | SQL 执行命令 |
| Netty | ChannelHandler | 命令封装 |
八、与其他模式的关系
与备忘录模式
- 命令模式常与备忘录模式结合使用,备忘录保存接收者的状态快照,用于撤销操作
与组合模式
- 宏命令(MacroCommand)本质上是命令模式的组合模式应用
与策略模式
- 相似:都是对行为/算法的封装
- 区别:策略模式关注算法替换(同一目标不同算法),命令模式关注请求封装(参数化、撤销、队列)
与观察者模式
- 命令模式可以作为观察者模式中通知传递的载体
九、面试常见问题
Q1:命令模式和策略模式有什么区别?
A:策略模式关注的是"如何做"(算法的不同实现),客户端知道所有策略并根据需要选择;命令模式关注的是"做什么"(将请求封装为对象),客户端不需要知道接收者。命令模式支持撤销操作,策略模式不支持。
Q2:如何实现多层撤销?
A:使用两个栈:undoStack 和 redoStack。每次执行命令时 push 到 undoStack,清空 redoStack。撤销时从 undoStack pop 并执行 undo,然后 push 到 redoStack。重做时从 redoStack pop 并执行 execute,然后 push 回 undoStack。
Q3:命令模式中如何处理命令执行失败的情况?
A:可以在 Command 接口中增加 rollback() 方法,或者在执行前保存状态快照(结合备忘录模式),执行失败时恢复状态。也可以使用事务机制,命令执行失败时自动回滚已执行的命令。
Q4:什么情况下不适合使用命令模式?
A:当操作非常简单且不需要撤销/重做功能时,命令模式可能过度设计。如果操作数量很少且不会扩展,直接调用可能更简单。性能敏感的场景,命令模式的多层抽象可能带来额外开销。
