文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

深入浅出设计模式【十四、命令模式】

一、命令模式介绍

命令模式的核心思想是将请求(或操作)封装为独立的对象。这个对象包含了执行该请求所需的所有信息(接收者、方法、参数)。

通过这种封装,命令的发出者(Invoker)和命令的执行者(Receiver)被彻底解耦。发出者不需要知道执行者是谁、执行了什么操作、以及操作如何实现。它只需要知道如何触发命令对象即可。这使得请求本身可以像其他对象一样被存储、传递、排队、记录和撤销,从而为构建灵活且功能丰富的系统奠定了基础。

二、核心概念与意图

  1. 核心概念

    • 命令 (Command): 声明执行操作的接口,通常包含一个 execute() 方法。
    • 具体命令 (Concrete Command): 实现命令接口。它通常持有对一个接收者对象的引用,并将调用委托给接收者的一个或多个方法。它定义了操作和接收者之间的绑定关系。
    • 客户端 (Client): 创建具体命令对象,并为其配置接收者(即,确定命令对象在 execute() 时应该调用哪个接收者的哪个方法)。
    • 调用者 (Invoker): 要求命令对象执行请求。它持有命令对象,并在某个时间点(如按钮被点击、定时器触发)调用命令对象的 execute() 方法。
    • 接收者 (Receiver): 知道如何执行与请求相关的操作。任何类都可以作为接收者。具体命令对象会将调用委托给它。
  2. 意图

    • 将一个请求封装为一个对象,从而使您可以用不同的请求对客户进行参数化
    • 对请求排队或记录请求日志,以及支持可撤销的操作
    • 将调用操作的对象与知道如何实现该操作的对象解耦

三、适用场景剖析

命令模式在以下场景中非常有效:

  1. 需要将操作作为参数进行传递时: 例如,需要将用户界面的一个操作(如点击按钮)配置为执行某个业务逻辑。命令对象可以完美地作为这个“操作”的载体。
  2. 需要支持操作的撤销 (Undo) 和重做 (Redo) 时: 这是命令模式的杀手级应用。通过存储已执行的命令列表,并在命令对象中实现 undo() 方法(通常与 execute() 逻辑相反),可以轻松实现历史记录和回滚功能。
  3. 需要支持事务(Transaction)语义时: 需要将一系列操作作为一个原子单元来执行。如果其中某个操作失败,可以回滚之前所有已执行的操作。命令对象可以记录所有操作,并在失败时触发一系列 undo() 操作。
  4. 需要将请求排队、调度执行或记录日志时: 命令对象可以被放入队列中,由工作线程按顺序执行(如线程池、任务队列)。也可以在被执行前记录日志,用于系统审计或故障恢复。
  5. 需要支持宏命令(Macro Command)时: 即用一个命令代表一系列其他命令的组合(组合模式 + 命令模式)。

四、UML 类图解析(Mermaid)

以下UML类图清晰地展示了命令模式的结构和角色间的关系:

creates
knows
knows
Client
Invoker
-command: Command
+setCommand(command: Command)
+executeCommand()
«interface»
Command
+execute()
+undo()
ConcreteCommand
-receiver: Receiver
-state
+execute()
+undo()
Receiver
+action()
  • Command (命令接口): 声明执行操作的接口,通常是 execute() 方法。为了实现撤销,通常还会包含 undo() 方法。
  • ConcreteCommand (具体命令)
    • 实现命令接口。
    • 持有对一个接收者对象的引用 (-receiver: Receiver)。
    • execute() 方法中,调用接收者的一个或多个方法(如 receiver.action())来完成具体的业务逻辑。
    • 可能存储执行前的状态 (-state),以便在 undo() 方法中能够将接收者恢复到执行前的状态。
  • Invoker (调用者)
    • 持有命令对象 (-command: Command)。
    • 提供设置命令的方法 (setCommand())。
    • 在特定时机(如 actionPerformed())调用命令对象的 execute() 方法 (executeCommand())。
    • 它不知道命令的具体内容,只负责触发。
  • Receiver (接收者)
    • 知道如何执行具体的操作,包含实际的业务逻辑(action() 方法)。
    • 具体命令对象会将调用委托给接收者。
  • Client (客户端)
    • 创建具体命令对象 (ConcreteCommand)。
    • 为命令对象配置接收者 (receiver)。
    • 将命令对象传递给调用者 (invoker.setCommand(command))。

五、各种实现方式及其优缺点

命令模式的实现关键在于如何设计命令接口和管理命令的生命周期。

1. 标准实现(接口 + 类)

即上述UML所描述的方式,为每个操作定义一个具体的命令类。

// 1. Command Interface
public interface Command {
    void execute();
    void undo(); // For undo functionality
}

// 2. Receiver
public class Light {
    public void on() {
        System.out.println("Light is ON");
    }
    public void off() {
        System.out.println("Light is OFF");
    }
}

// 3. Concrete Command
public class LightOnCommand implements Command {
    private Light light; // The receiver

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on(); // Delegate to the receiver
    }

    @Override
    public void undo() {
        light.off(); // The undo action is the opposite
    }
}

// 4. Invoker (e.g., a remote control with one button)
public class SimpleRemoteControl {
    private Command slot; // Holds one command

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

    public void buttonWasPressed() {
        slot.execute(); // Executes the currently set command
    }
}

// 5. Client
public class Client {
    public static void main(String[] args) {
        SimpleRemoteControl remote = new SimpleRemoteControl(); // Invoker
        Light light = new Light(); // Receiver
        LightOnCommand lightOn = new LightOnCommand(light); // ConcreteCommand

        remote.setCommand(lightOn); // Client configures the invoker with a command
        remote.buttonWasPressed(); // Invoker triggers the command
    }
}
  • 优点
    • 解耦彻底: 调用者与接收者完全解耦。
    • 易于扩展: 添加新命令只需实现新的具体命令类,符合开闭原则。
    • 功能强大: 天然支持宏命令、队列、日志、撤销/重做等高级功能。
  • 缺点
    • 类爆炸 (Class Bloat): 如果系统有大量操作,会产生许多具体命令类,增加系统复杂度。

2. 函数式命令(利用Lambda表达式或方法引用)

在Java 8+中,如果命令接口只包含一个方法(通常是 execute()),可以利用函数式接口和Lambda表达式来简化实现,避免为每个命令创建单独的类。

// Command is now a Functional Interface
@FunctionalInterface
public interface Command {
    void execute();
}

// Receiver remains the same
public class Light {
    public void on() { System.out.println("Light is ON"); }
    public void off() { System.out.println("Light is OFF"); }
}

// Invoker remains the same
public class SimpleRemoteControl {
    private Command command;
    public void setCommand(Command command) { this.command = command; }
    public void buttonWasPressed() { command.execute(); }
}

// Client uses Lambda expressions or method references
public class Client {
    public static void main(String[] args) {
        SimpleRemoteControl remote = new SimpleRemoteControl();
        Light light = new Light();

        // Instead of creating a concrete class, use a lambda
        remote.setCommand(() -> light.on()); // Set command to turn light on

        remote.buttonWasPressed();

        // Can also use method references if the signature matches
        remote.setCommand(light::off); // Set command to turn light off
        remote.buttonWasPressed();
    }
}
  • 优点
    • 代码简洁: 极大地减少了样板代码,避免了类爆炸问题。
    • 灵活直观: 在客户端就地定义行为,非常直观。
  • 缺点
    • 局限性: 难以实现复杂的命令(如需要存储状态以实现 undo())。undo() 功能很难用简单的Lambda实现,除非引入更复杂的结构。
    • 可读性: 复杂的逻辑写在Lambda中可能降低可读性。

选择建议: 对于简单的、不需要撤销功能的命令,优先使用函数式实现。对于需要状态管理、撤销/重做、事务等复杂功能的命令,使用标准的类实现。

六、最佳实践

  1. 实现撤销 (Undo) 操作

    • Command 接口中添加 undo() 方法。
    • undo() 的实现通常是 execute() 的逆操作。
    • 调用者(或一个专门的 History 对象)需要维护一个已执行命令的栈(Stack)。执行命令时,将其压入栈。执行撤销时,弹出栈顶命令并调用其 undo() 方法。
    public class RemoteControlWithUndo {
        private Command lastExecutedCommand;
    
        public void setAndExecuteCommand(Command command) {
            command.execute();
            lastExecutedCommand = command; // Store for undo
        }
    
        public void undoButtonWasPressed() {
            if (lastExecutedCommand != null) {
                lastExecutedCommand.undo();
            }
        }
    }
    
  2. 实现宏命令 (Macro Command)

    • 宏命令也是一个具体命令,但它包含一个命令列表。
    • execute() 方法会遍历并执行列表中的所有命令。
    • undo() 方法通常会以相反的顺序执行所有命令的 undo()(注意:实现一个完全正确的宏撤销可能很复杂)。
    public class MacroCommand implements Command {
        private List<Command> commands;
    
        public MacroCommand(List<Command> commands) {
            this.commands = commands;
        }
    
        @Override
        public void execute() {
            for (Command command : commands) {
                command.execute();
            }
        }
    
        @Override
        public void undo() {
            // Undo in reverse order
            for (int i = commands.size() - 1; i >= 0; i--) {
                commands.get(i).undo();
            }
        }
    }
    
  3. 与备忘录模式 (Memento) 结合: 对于复杂的撤销操作,如果恢复状态很困难,可以让命令对象在执行前从接收者获取一个状态的备忘录(Memento),并在撤销时使用该备忘录来恢复状态。

  4. 空对象 (Null Object) 应用: 在调用者中,可以初始化一个什么都不做的空命令(NoCommand),避免对 null 进行检查。

    public class NoCommand implements Command {
        @Override
        public void execute() { /* Do nothing */ }
        @Override
        public void undo() { /* Do nothing */ }
    }
    // In Invoker:
    // private Command slot = new NoCommand(); // Default to no command
    

七、在开发中的演变和应用

命令模式的思想是现代异步编程和系统架构的核心:

  1. 任务队列与线程池RunnableCallable 对象本质上就是命令对象。它们被提交到 ExecutorService(调用者),由线程池中的线程(接收者)执行。这实现了任务的提交与执行的解耦。
  2. 消息队列与事件驱动架构: 发送到消息队列(如RabbitMQ、Kafka)中的消息可以看作是序列化的命令对象。消费者接收到消息后,将其反序列化并执行其中包含的命令。这是分布式环境下的命令模式。
  3. 事务脚本与工作单元: 在数据库事务中,一系列操作可以被组织成命令对象。如果所有操作成功,则提交事务;如果任何一个失败,则回滚所有操作(执行每个命令的补偿操作)。
  4. 异步操作与回调: 在GUI编程或网络编程中,将一个操作(命令)提交给后台线程执行,并在操作完成后执行另一个操作(回调命令),这是一种常见的模式。

八、真实开发案例(Java语言内部、知名开源框架、工具)

  1. Java Runnable 接口

    • Runnable 接口就是一个最经典的命令接口,它只定义了一个方法 run()
    • Thread 类(调用者)持有 Runnable 命令,并在调用 start() 后最终执行 run() 方法。
    • ExecutorService 是更高级的调用者,它管理着一个线程池来执行提交的 RunnableCallable 命令。
  2. Swing/AWT 的 Action 接口

    • Java Swing 中的 Action 接口扩展了 ActionListener,它是一个丰富的命令接口,不仅定义了 actionPerformed() 方法(即 execute()),还包含了命令的文本、图标、启用状态等元信息。
    • JButtonJMenuItem 等组件(调用者)可以设置一个 Action 命令。组件会自动根据 Action 的元信息来更新自己的显示状态。
  3. Spring Framework 的 JdbcTemplate

    • 虽然不完全是标准的命令模式,但其思想高度吻合。JdbcTemplateexecute(ConnectionCallback), query(PreparedStatementCreator, RowMapper) 等方法接受各种回调接口。
    • 这些回调接口(如 ConnectionCallback, PreparedStatementCreator)就是命令接口,定义了如何在JDBC连接的上下文中执行操作。
    • 开发者提供这些接口的具体实现(具体命令),JdbcTemplate(调用者)负责管理连接、语句等资源,并在正确的时机调用这些命令。这完美地将可变的部分(SQL操作)与不变的部分(资源管理)分离开来。
  4. Hibernate/JPA 的 @PostPersist, @PreUpdate 等监听器

    • 可以将这些注解标记的方法视为一种“命令”,Hibernate(调用者)在特定的生命周期事件(如持久化前、更新后)自动触发执行这些命令。
  5. 项目中的工作流引擎或审批系统

    • 每个审批操作(通过、驳回、转交)都可以封装成一个命令对象。工作流引擎(调用者)根据流程定义来执行相应的命令。

九、总结

方面总结
模式类型行为型设计模式
核心意图将请求封装为对象,从而使您可以用不同的请求参数化客户端,并支持请求的排队、记录、撤销。
关键角色命令(Command), 具体命令(ConcreteCommand), 调用者(Invoker), 接收者(Receiver), 客户端(Client)
核心机制封装与委托: 将请求细节封装在命令对象中。调用者触发命令,命令委托接收者执行操作。
主要优点1. 解耦: 解耦了请求发送者和接收者。
2. 灵活性高: 新命令易扩展,命令可组合(宏命令)。
3. 支持高级功能: 天然支持撤销、事务、队列、日志。
主要缺点1. 类爆炸: 可能产生大量具体的命令类(可用Lambda缓解)。
2. 复杂度增加: 引入新的抽象层。
适用场景1. 需要支持撤销/重做、事务。
2. 需要将操作排队、记录日志、或远程执行。
3. 需要用不同操作参数化对象(如GUI按钮)。
4. 需要支持宏命令。
实现选择标准类实现: 功能强大,支持复杂操作(撤销、状态)。
函数式实现 (Lambda): 简洁灵活,适用于简单命令。
最佳实践使用空对象;与备忘录模式结合实现复杂撤销;利用宏命令组合操作。
现代应用异步任务 (Runnable)消息队列事务管理事件驱动架构的理论基础。
真实案例Java Runnable/Callable (核心),Swing Action (GUI),Spring JdbcTemplate 回调 (资源管理)。

命令模式通过将“操作”提升为一等公民(对象),赋予了请求前所未有的灵活性和控制力。它是实现撤销、事务、任务队列等高级功能的基石,深刻影响着从GUI编程到分布式系统设计的方方面面。理解并掌握命令模式,是构建复杂、灵活、可维护系统的关键技能,它教会我们如何将“做什么”和“谁来做”、“何时做”有效地分离开来。

posted @ 2025-08-29 13:24  NeoLshu  阅读(3)  评论(0)    收藏  举报  来源