Skip to content

命令模式 (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)
  • 需要将操作记录到日志中
  • 需要将操作放入队列异步执行

如果直接在按钮的点击事件中写业务逻辑,会导致:

  1. 调用者与执行者紧密耦合
  2. 无法支持撤销操作
  3. 难以扩展新的操作

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 "已撤销最后一个操作";
    }
}

五、优缺点

优点

  1. 解耦调用者和接收者:调用者只知道命令接口,不需要知道具体实现
  2. 易于扩展:新增命令只需实现 Command 接口,符合开闭原则
  3. 支持撤销/重做:通过存储命令历史实现 Undo/Redo
  4. 支持命令队列:可以将命令排队、异步执行
  5. 支持组合命令:宏命令可以组合多个简单命令

缺点

  1. 类膨胀:每个具体操作都需要一个命令类,类数量会大量增加
  2. 复杂度增加:引入了多层抽象,增加了系统复杂度
  3. 内存开销:维护命令历史需要额外的内存空间

六、适用场景

  1. 需要撤销/重做功能:文本编辑器、IDE、图片编辑器
  2. 操作需要排队执行:消息队列、任务调度
  3. 操作需要记录日志:审计系统、操作日志记录
  4. GUI 按钮与菜单:按钮、菜单项与操作的解耦
  5. 事务操作:数据库事务的提交和回滚
  6. 分布式任务:将操作序列化后发送到远程执行

七、JDK / Spring 框架中的实际应用

框架应用位置说明
JDKjava.lang.Runnable命令接口,Thread 是调用者
JDKjavax.swing.ActionSwing 中的命令模式
SpringJdbcTemplatePreparedStatementCallback 是命令
Spring MVC@RequestMapping方法级别的命令映射
MyBatisExecutorSQL 执行命令
NettyChannelHandler命令封装

八、与其他模式的关系

与备忘录模式

  • 命令模式常与备忘录模式结合使用,备忘录保存接收者的状态快照,用于撤销操作

与组合模式

  • 宏命令(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:当操作非常简单且不需要撤销/重做功能时,命令模式可能过度设计。如果操作数量很少且不会扩展,直接调用可能更简单。性能敏感的场景,命令模式的多层抽象可能带来额外开销。