命令设计模式全方位深度解析

6 阅读8分钟

一、核心概念

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

设计思想

  • 解耦请求发送者与接收者:发送者不需要知道接收者的具体接口
  • 将"做什么"和"谁来做"分离:命令对象封装了执行操作所需的所有信息
  • 支持事务性操作:命令可以支持撤销(undo)和重做(redo)

二、模式结构

类图示意

复制
┌─────────────────┐       ┌───────────────┐       ┌───────────────┐
│    Client       │───────│    Invoker    │       │   Receiver    │
└─────────────────┘       └───────────────┘       └───────────────┘
         │                         │                       │
         │                         │  commands            │
         │                         ▼                       │
         │               ┌─────────────────┐               │
         └───────────────│   Command       │◄──────────────┘
                         │  (Interface)    │
                         │ +execute()      │
                         │ +undo()         │
                         └─────────────────┘
                                 △
                ┌────────────────┼────────────────┐
                │                                 │
    ┌─────────────────────┐           ┌─────────────────────┐
    │  ConcreteCommandA   │           │  ConcreteCommandB   │
    │ -receiver: Receiver │           │ -receiver: Receiver │
    │ +execute()          │           │ +execute()          │
    │ +undo()             │           │ +undo()             │
    └─────────────────────┘           └─────────────────────┘

核心角色

  1. Command (命令接口)

    • 声明执行操作的接口
  2. ConcreteCommand (具体命令)

    • 实现Command接口
    • 将接收者绑定到动作
    • 调用接收者的操作
  3. Invoker (调用者)

    • 要求命令执行请求
    • 持有命令对象
  4. Receiver (接收者)

    • 知道如何执行操作
    • 实际的工作执行者
  5. Client (客户端)

    • 创建具体命令并设置接收者

三、Java代码实现

基础实现示例

java
java
下载
复制
// 1. 命令接口
public interface Command {
    void execute();
    void undo();
}

// 2. 接收者 - 实际执行操作
public class TextEditor {
    private StringBuilder text = new StringBuilder();
    private int cursorPosition = 0;
    
    public void insert(String newText) {
        text.insert(cursorPosition, newText);
        cursorPosition += newText.length();
        System.out.println("插入文本: " + newText);
        System.out.println("当前文本: " + text);
    }
    
    public void delete(int length) {
        if (cursorPosition - length >= 0) {
            int start = cursorPosition - length;
            String deleted = text.substring(start, cursorPosition);
            text.delete(start, cursorPosition);
            cursorPosition = start;
            System.out.println("删除文本: " + deleted);
            System.out.println("当前文本: " + text);
        }
    }
    
    public void setCursor(int position) {
        this.cursorPosition = Math.max(0, Math.min(position, text.length()));
    }
    
    public String getText() {
        return text.toString();
    }
}

// 3. 具体命令 - 插入文本
public class InsertCommand implements Command {
    private TextEditor editor;
    private String text;
    private int position;
    
    public InsertCommand(TextEditor editor, String text) {
        this.editor = editor;
        this.text = text;
        this.position = 0; // 简化处理
    }
    
    @Override
    public void execute() {
        editor.insert(text);
    }
    
    @Override
    public void undo() {
        editor.delete(text.length());
    }
}

// 4. 具体命令 - 删除文本
public class DeleteCommand implements Command {
    private TextEditor editor;
    private int length;
    private String deletedText; // 用于撤销
    
    public DeleteCommand(TextEditor editor, int length) {
        this.editor = editor;
        this.length = length;
    }
    
    @Override
    public void execute() {
        // 实际应用中需要保存被删除的文本以便撤销
        editor.delete(length);
    }
    
    @Override
    public void undo() {
        // 重新插入被删除的文本
        editor.insert(deletedText);
    }
}

// 5. 调用者 - 支持撤销/重做
public class CommandInvoker {
    private Stack<Command> history = new Stack<>();
    private Stack<Command> redoStack = new Stack<>();
    
    public void executeCommand(Command command) {
        command.execute();
        history.push(command);
        redoStack.clear(); // 执行新命令时清空重做栈
    }
    
    public void undo() {
        if (!history.isEmpty()) {
            Command command = history.pop();
            command.undo();
            redoStack.push(command);
        }
    }
    
    public void redo() {
        if (!redoStack.isEmpty()) {
            Command command = redoStack.pop();
            command.execute();
            history.push(command);
        }
    }
    
    public void showHistory() {
        System.out.println("命令历史:");
        for (Command cmd : history) {
            System.out.println("  " + cmd.getClass().getSimpleName());
        }
    }
}

// 6. 客户端使用
public class CommandPatternDemo {
    public static void main(String[] args) {
        // 创建接收者
        TextEditor editor = new TextEditor();
        
        // 创建调用者
        CommandInvoker invoker = new CommandInvoker();
        
        // 创建并执行命令
        Command insertHello = new InsertCommand(editor, "Hello ");
        invoker.executeCommand(insertHello);
        
        Command insertWorld = new InsertCommand(editor, "World!");
        invoker.executeCommand(insertWorld);
        
        // 撤销操作
        System.out.println("\n--- 执行撤销 ---");
        invoker.undo();
        
        // 重做操作
        System.out.println("\n--- 执行重做 ---");
        invoker.redo();
        
        // 显示历史
        invoker.showHistory();
    }
}

高级示例:支持宏命令

java
java
下载
复制
// 宏命令 - 组合多个命令
public class MacroCommand implements Command {
    private List<Command> commands = new ArrayList<>();
    
    public void addCommand(Command command) {
        commands.add(command);
    }
    
    public void removeCommand(Command command) {
        commands.remove(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();
        }
    }
}

// 客户端使用宏命令
public class MacroCommandDemo {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        CommandInvoker invoker = new CommandInvoker();
        
        // 创建宏命令
        MacroCommand initDocument = new MacroCommand();
        initDocument.addCommand(new InsertCommand(editor, "标题\n"));
        initDocument.addCommand(new InsertCommand(editor, "===正文===\n"));
        initDocument.addCommand(new InsertCommand(editor, "第一段内容"));
        
        // 执行宏命令
        invoker.executeCommand(initDocument);
        
        System.out.println("最终文本: " + editor.getText());
    }
}

四、应用场景

1. GUI操作与按钮事件

java
java
下载
复制
// 按钮点击事件
public class Button {
    private Command command;
    
    public void setCommand(Command command) {
        this.command = command;
    }
    
    public void onClick() {
        if (command != null) {
            command.execute();
        }
    }
}

// 菜单项
public class MenuItem {
    private Command command;
    
    public void setCommand(Command command) {
        this.command = command;
    }
    
    public void onSelect() {
        command.execute();
    }
}

2. 任务队列与线程池

java
java
下载
复制
public class TaskScheduler {
    private Queue<Command> taskQueue = new LinkedList<>();
    
    public void addTask(Command task) {
        taskQueue.offer(task);
    }
    
    public void processTasks() {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        
        while (!taskQueue.isEmpty()) {
            Command task = taskQueue.poll();
            executor.submit(() -> {
                try {
                    task.execute();
                } catch (Exception e) {
                    // 记录日志,但不影响其他任务
                    System.err.println("任务执行失败: " + e.getMessage());
                }
            });
        }
        
        executor.shutdown();
    }
}

3. 事务性系统

java
java
下载
复制
// 数据库事务命令
public class TransactionManager {
    private List<Command> commands = new ArrayList<>();
    private List<Command> executedCommands = new ArrayList<>();
    
    public void addCommand(Command command) {
        commands.add(command);
    }
    
    public void commit() {
        try {
            for (Command cmd : commands) {
                cmd.execute();
                executedCommands.add(cmd);
            }
            commands.clear();
        } catch (Exception e) {
            rollback();
            throw new RuntimeException("事务提交失败", e);
        }
    }
    
    public void rollback() {
        for (int i = executedCommands.size() - 1; i >= 0; i--) {
            executedCommands.get(i).undo();
        }
        executedCommands.clear();
    }
}

4. 远程过程调用(RPC)

java
java
下载
复制
// 网络命令
public class NetworkCommand implements Command, Serializable {
    private String serviceName;
    private String methodName;
    private Object[] parameters;
    
    @Override
    public void execute() {
        // 通过网络发送命令到服务器执行
        RemoteExecutor.execute(this);
    }
    
    @Override
    public void undo() {
        // 远程撤销操作
        RemoteExecutor.undo(this);
    }
}

5. 游戏开发

java
java
下载
复制
// 游戏动作命令
public class GameActionCommand implements Command {
    private GameCharacter character;
    private Action action;
    private Position previousPosition;
    
    @Override
    public void execute() {
        previousPosition = character.getPosition();
        character.performAction(action);
    }
    
    @Override
    public void undo() {
        character.setPosition(previousPosition);
        character.undoAction(action);
    }
}

五、优缺点分析

优点

  1. 单一职责原则:解耦触发操作的类与执行操作的对象
  2. 开闭原则:无需修改现有代码即可添加新命令
  3. 支持撤销/重做:轻松实现操作的回滚
  4. 命令队列:支持延迟执行、任务调度
  5. 宏命令:支持组合简单命令创建复杂操作
  6. 易于测试:命令对象可以独立测试

缺点

  1. 类数量增加:每个具体命令都需要一个单独的类
  2. 复杂度增加:简单操作使用命令模式可能过度设计
  3. 内存占用:维护命令历史可能消耗较多内存
  4. 执行效率:间接调用比直接调用效率稍低

六、改进与变体

1. 使用函数式接口简化(Java 8+)

java
java
下载
复制
@FunctionalInterface
public interface SimpleCommand {
    void execute();
    
    default SimpleCommand andThen(SimpleCommand after) {
        return () -> {
            execute();
            after.execute();
        };
    }
}

// 使用Lambda表达式
public class FunctionalCommandDemo {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        
        SimpleCommand insert = () -> editor.insert("Hello");
        SimpleCommand append = () -> editor.insert(" World");
        
        // 链式调用
        insert.andThen(append).execute();
    }
}

2. 支持参数的命令

java
java
下载
复制
public interface ParamCommand<T> {
    void execute(T parameter);
    void undo(T parameter);
}

public class InsertAtCommand implements ParamCommand<String> {
    private TextEditor editor;
    private int position;
    
    @Override
    public void execute(String text) {
        editor.setCursor(position);
        editor.insert(text);
    }
    
    @Override
    public void undo(String text) {
        editor.delete(text.length());
    }
}

3. 命令工厂模式

java
java
下载
复制
public class CommandFactory {
    private Map<String, Supplier<Command>> commands = new HashMap<>();
    
    public CommandFactory() {
        // 注册命令
        commands.put("insert", InsertCommand::new);
        commands.put("delete", DeleteCommand::new);
    }
    
    public Command createCommand(String type, Object... args) {
        Supplier<Command> supplier = commands.get(type);
        if (supplier != null) {
            // 这里需要根据参数动态创建命令实例
            return supplier.get();
        }
        throw new IllegalArgumentException("未知命令类型: " + type);
    }
}

4. 持久化命令

java
java
下载
复制
public abstract class PersistentCommand implements Command, Serializable {
    private Date timestamp = new Date();
    private String userId;
    
    public abstract String toLogString();
    
    public void saveToDatabase() {
        // 保存命令到数据库
        Database.save(this.getClass().getSimpleName(), 
                     toLogString(), timestamp, userId);
    }
}

七、注意事项

1. 避免过度设计

  • 简单操作不要使用命令模式
  • 只在确实需要解耦、撤销、队列等功能时使用

2. 内存管理

  • 命令历史可能导致内存泄漏
  • 考虑实现命令的序列化以存储到磁盘
  • 设置命令历史的最大长度
java
java
下载
复制
public class LimitedHistoryInvoker extends CommandInvoker {
    private final int maxHistorySize;
    
    public LimitedHistoryInvoker(int maxHistorySize) {
        this.maxHistorySize = maxHistorySize;
    }
    
    @Override
    public void executeCommand(Command command) {
        super.executeCommand(command);
        
        // 限制历史记录大小
        if (history.size() > maxHistorySize) {
            // 移除最旧的命令
            // 注意:可能需要特殊处理,如合并命令或持久化到磁盘
        }
    }
}

3. 线程安全

java
java
下载
复制
public class ThreadSafeInvoker {
    private final Stack<Command> history = new Stack<>();
    private final Lock lock = new ReentrantLock();
    
    public void executeCommand(Command command) {
        lock.lock();
        try {
            command.execute();
            history.push(command);
        } finally {
            lock.unlock();
        }
    }
}

4. 错误处理

java
java
下载
复制
public interface RobustCommand extends Command {
    boolean canExecute();
    boolean canUndo();
    void onExecuteError(Exception e);
    void onUndoError(Exception e);
}

5. 性能考虑

  • 对于高频操作,考虑使用对象池
  • 避免在命令中存储大量数据
  • 考虑使用享元模式共享接收者状态

八、与其他模式的关系

与策略模式对比

  • 命令模式:关注"做什么"和"何时做",强调动作的封装和执行时机
  • 策略模式:关注"如何做",强调算法或策略的替换

与备忘录模式结合

java
java
下载
复制
// 结合备忘录实现更完善的撤销
public class MementoCommand implements Command {
    private Receiver receiver;
    private Memento memento;
    
    @Override
    public void execute() {
        memento = receiver.createMemento();
        // 执行操作
    }
    
    @Override
    public void undo() {
        receiver.restoreMemento(memento);
    }
}

与组合模式结合

  • 宏命令是组合模式的典型应用
  • 可以创建命令树结构

九、实际应用建议

适用场景

  1. 需要操作队列的场景:批处理、任务调度
  2. 需要支持撤销/重做的场景:文本编辑器、图形编辑器
  3. 需要记录操作日志的场景:审计系统、操作追踪
  4. 需要解耦调用者和接收者的场景:GUI框架、事件处理
  5. 需要支持事务的场景:数据库操作、支付系统

不适用场景

  1. 简单的一次性操作
  2. 性能要求极高的场景
  3. 命令间无共同接口的场景

最佳实践

  1. 尽量使命令接口简单,通常只需要execute()undo()方法
  2. 考虑使用泛型使命令更灵活
  3. 为复杂命令提供构建器
  4. 使用依赖注入管理命令依赖
  5. 为生产环境添加适当的日志和监控

十、总结

命令模式通过将请求封装为对象,提供了强大的灵活性和可扩展性。它在需要支持撤销、事务、队列、日志等功能的系统中表现出色。虽然会引入额外的复杂性,但在适当的场景下,它能显著提高代码的可维护性和可扩展性。

现代Java开发中,可以结合函数式编程特性简化命令模式的实现,同时保持其核心优势。在实际应用中,应根据具体需求权衡使用,避免不必要的过度设计。