设计模式之命令模式

概念

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

定义

命令模式把一个请求或者操作封装到一个对象中,将发出命令的责任和执行命令的责任分割开。这样,命令的发送者不需要知道命令的接收者是谁,也不需要知道命令是如何被执行的,只需要关心如何发出命令。而命令的接收者只需要专注于如何执行命令,实现具体的业务逻辑。

想象一下,此时你手里握着一个智能家居的遥控器,可以控制家里的各种设备(灯、空调、音响)。最初,遥控器的每个按钮直接绑定了设备的操作------比如"开灯"按钮直接硬编码调用了客厅灯.turnOn() 的方法。这种方法看似简单,但是当设备升级的时候(比如换用语音控制的智能灯),你就需要拆开遥控器重新焊接电路;想新增一个"观影模式"(开灯、开空调、关窗帘)时,你又需要继续再遥控器内新增一堆新逻辑。而命令模式(Command) 则是把每一个操作(如开灯、调节温度)封装成独立的 命令对象 ,例如LightOnCommand包含执行(execute())和撤销(undo())方法,内部持有对灯具的引用。遥控器(调用者)完全无需知道设备细节,只需存储并触发这些命令对象。当你按下"开灯"按钮时,遥控器只是调用lightCommand.execute(),具体是传统灯具还是智能灯执行,它毫不关心。

那么,在这样的设计模式下,就会体现出下述三个优点:

  1. 灵活扩展 :新增空调控制,只需创建一个AirconCommand丢给遥控器,无需改动原有代码;
  2. 支持宏命令 :将关灯、开空调、降窗帘组合成MovieModeCommand,一键触发复杂操作;
  3. 实现撤销功能:执行命令后,遥控器记录历史,按下"撤销"键即可回退到上一步状态。

这就是命令模式的核心------将"请求"抽象为对象,让调用者和接收者解耦。

组成部分

  • 命令接口(Command) :声明了执行命令的抽象方法,所有具体命令类都需要实现这个接口。它定义了一个统一的执行命令的方法,通常命名为execute等,用来规范具体命令类的行为。
  • 具体命令类(ConcreteCommand) :实现了命令接口,持有一个接收者对象的引用,在execute方法中调用接收者的相关方法来完成具体的操作。它将命令的执行和接收者的具体行为绑定在一起,实现了命令的具体逻辑。
  • 接收者(Receiver):知道如何执行与请求相关的操作,具体命令对象会调用它的方法来完成命令的执行。它是真正执行命令的对象,负责实现命令所对应的具体业务逻辑。
  • 调用者(Invoker) :负责调用命令对象的execute方法来发起命令。它不直接与接收者交互,而是通过命令对象来间接执行操作,它可以设置命令对象,并在需要的时候触发命令的执行。
  • 客户端(Client):创建具体命令对象,并设置命令对象的接收者。在客户端中,将命令的发送者和接收者进行解耦,使得发送者不需要了解接收者的具体实现,只需要关心命令的发送。

工作原理

  1. 定义接口 :先定义一个命令接口,声明execute等方法规范命令行为。
  2. 创建具体命令类 :创建实现命令接口的具体命令类,内部持有接收者对象,在execute方法中调用接收者方法完成具体操作。
  3. 设置接收者:创建接收者对象,它负责执行具体业务逻辑,具体命令类将其作为成员变量持有。
  4. 设置调用者 :创建调用者,它持有命令对象,通过调用命令对象的execute方法来发起命令,并不直接与接收者交互。
  5. 客户端操作:客户端负责创建具体命令对象,设置好接收者,并将命令对象传递给调用者。
  6. 命令执行:调用者执行命令时,命令对象调用接收者的相应方法完成操作,实现请求发送者和接收者的解耦。若支持撤销,命令对象会记录执行前状态,在撤销时调用接收者方法恢复状态。

示例

我么使用智能家居遥控器的例子来编写示例代码。

类图

C++实现

C++ 复制代码
#include <iostream>
#include <vector>

// 前向声明
class Receiver;

// 命令接口
class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() = default;
};

// 接收者:灯具
class Light {
public:
    void turnOn() {
        std::cout << "Light is on." << std::endl;
    }
    void turnOff() {
        std::cout << "Light is off." << std::endl;
    }
};

// 接收者:空调
class AirConditioner {
public:
    void turnOn() {
        std::cout << "Air conditioner is on." << std::endl;
    }
    void turnOff() {
        std::cout << "Air conditioner is off." << std::endl;
    }
};

// 开灯命令
class LightOnCommand : public Command {
private:
    Light* light;
public:
    explicit LightOnCommand(Light* light) : light(light) {}
    void execute() override {
        light->turnOn();
    }
    void undo() override {
        light->turnOff();
    }
};

// 关灯命令
class LightOffCommand : public Command {
private:
    Light* light;
public:
    explicit LightOffCommand(Light* light) : light(light) {}
    void execute() override {
        light->turnOff();
    }
    void undo() override {
        light->turnOn();
    }
};

// 开空调命令
class AirConditionerOnCommand : public Command {
private:
    AirConditioner* ac;
public:
    explicit AirConditionerOnCommand(AirConditioner* ac) : ac(ac) {}
    void execute() override {
        ac->turnOn();
    }
    void undo() override {
        ac->turnOff();
    }
};

// 关空调命令
class AirConditionerOffCommand : public Command {
private:
    AirConditioner* ac;
public:
    explicit AirConditionerOffCommand(AirConditioner* ac) : ac(ac) {}
    void execute() override {
        ac->turnOff();
    }
    void undo() override {
        ac->turnOn();
    }
};

// 调用者:遥控器
class RemoteControl {
private:
    std::vector<Command*> commands;
    std::vector<Command*> undoCommands;
public:
    void setCommand(Command* command) {
        commands.push_back(command);
    }
    void pressButton(int index) {
        if (index < commands.size()) {
            commands[index]->execute();
            undoCommands.push_back(commands[index]);
        }
    }
    void pressUndoButton() {
        if (!undoCommands.empty()) {
            Command* lastCommand = undoCommands.back();
            lastCommand->undo();
            undoCommands.pop_back();
        }
    }
    ~RemoteControl() {
        for (auto command : commands) {
            delete command;
        }
    }
};

// 客户端代码
int main() {
    // 创建接收者
    Light light;
    AirConditioner ac;

    // 创建命令
    Command* lightOn = new LightOnCommand(&light);
    Command* lightOff = new LightOffCommand(&light);
    Command* acOn = new AirConditionerOnCommand(&ac);
    Command* acOff = new AirConditionerOffCommand(&ac);

    // 创建调用者
    RemoteControl remote;

    // 设置命令
    remote.setCommand(lightOn);
    remote.setCommand(lightOff);
    remote.setCommand(acOn);
    remote.setCommand(acOff);

    // 执行命令
    remote.pressButton(0); // 开灯
    remote.pressButton(2); // 开空调
    remote.pressUndoButton(); // 撤销开空调
    remote.pressUndoButton(); // 撤销开灯

    return 0;
}

Java实现

Java 复制代码
// 命令接口
interface Command {
    void execute();
    void undo();
}

// 接收者:灯具
class Light {
    public void turnOn() {
        System.out.println("Light is on.");
    }

    public void turnOff() {
        System.out.println("Light is off.");
    }
}

// 接收者:空调
class AirConditioner {
    public void turnOn() {
        System.out.println("Air conditioner is on.");
    }

    public void turnOff() {
        System.out.println("Air conditioner is off.");
    }
}

// 开灯命令
class LightOnCommand implements Command {
    private Light light;

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

    @Override
    public void execute() {
        light.turnOn();
    }

    @Override
    public void undo() {
        light.turnOff();
    }
}

// 关灯命令
class LightOffCommand implements Command {
    private Light light;

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

    @Override
    public void execute() {
        light.turnOff();
    }

    @Override
    public void undo() {
        light.turnOn();
    }
}

// 开空调命令
class AirConditionerOnCommand implements Command {
    private AirConditioner ac;

    public AirConditionerOnCommand(AirConditioner ac) {
        this.ac = ac;
    }

    @Override
    public void execute() {
        ac.turnOn();
    }

    @Override
    public void undo() {
        ac.turnOff();
    }
}

// 关空调命令
class AirConditionerOffCommand implements Command {
    private AirConditioner ac;

    public AirConditionerOffCommand(AirConditioner ac) {
        this.ac = ac;
    }

    @Override
    public void execute() {
        ac.turnOff();
    }

    @Override
    public void undo() {
        ac.turnOn();
    }
}

// 调用者:遥控器
class RemoteControl {
    private java.util.ArrayList<Command> commands = new java.util.ArrayList<>();
    private java.util.ArrayList<Command> undoCommands = new java.util.ArrayList<>();

    public void setCommand(Command command) {
        commands.add(command);
    }

    public void pressButton(int index) {
        if (index < commands.size()) {
            Command command = commands.get(index);
            command.execute();
            undoCommands.add(command);
        }
    }

    public void pressUndoButton() {
        if (!undoCommands.isEmpty()) {
            Command lastCommand = undoCommands.remove(undoCommands.size() - 1);
            lastCommand.undo();
        }
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        // 创建接收者
        Light light = new Light();
        AirConditioner ac = new AirConditioner();

        // 创建命令
        Command lightOn = new LightOnCommand(light);
        Command lightOff = new LightOffCommand(light);
        Command acOn = new AirConditionerOnCommand(ac);
        Command acOff = new AirConditionerOffCommand(ac);

        // 创建调用者
        RemoteControl remote = new RemoteControl();

        // 设置命令
        remote.setCommand(lightOn);
        remote.setCommand(lightOff);
        remote.setCommand(acOn);
        remote.setCommand(acOff);

        // 执行命令
        remote.pressButton(0); // 开灯
        remote.pressButton(2); // 开空调
        remote.pressUndoButton(); // 撤销开空调
        remote.pressUndoButton(); // 撤销开灯
    }
}

代码解释

  1. Command 接口 :定义了命令的基本操作,包括execute()用于执行命令和undo()用于撤销命令。
  2. 接收者类(Light 和 AirConditioner) :这些类表示具体的设备,包含设备的基本操作方法,如turnOn()turnOff()
  3. 具体命令类(LightOnCommand、LightOffCommand、AirConditionerOnCommand、AirConditionerOffCommand) :实现了Command接口,内部持有对接收者的引用,在execute()方法中调用接收者的相应操作,undo()方法则执行相反的操作。
  4. 调用者类(RemoteControl) :负责存储和执行命令,通过setCommand()方法设置命令,pressButton()方法执行指定索引的命令,pressUndoButton()方法撤销上一个执行的命令。
  5. 客户端代码(main 函数):创建接收者、命令和调用者对象,设置命令并执行相应操作。

通过这种方式,遥控器(调用者)不需要知道具体设备的细节,只需要操作命令对象,提高了系统的可扩展性和可维护性。当设备升级或新增功能时,只需要创建新的命令类,而不需要修改遥控器的代码。

设计原则

命令模式主要遵循以下几个重要的设计原则:

单一职责原则(Single Responsibility Principle)

  • 解释:该原则强调一个类应该仅有一个引起它变化的原因。在命令模式中,每个类都有其明确的职责。
  • 具体体现
    • 命令接口和具体命令类Command接口只负责定义命令的执行和撤销等操作规范,而具体的命令类(如LightOnCommandLightOffCommand)只负责实现特定的命令逻辑,比如在execute方法中调用接收者的特定操作,它们不涉及其他无关的功能。
    • 接收者类 :像LightAirConditioner类,只专注于自身设备的具体操作,如开启、关闭等,不参与命令的管理和调用逻辑。
    • 调用者类RemoteControl类只负责存储和执行命令,不关心命令具体如何实现以及接收者的具体操作细节。

开闭原则(Open/Closed Principle)

  • 解释:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即当需求发生变化时,应该通过扩展软件实体的行为来实现,而不是修改已有的代码。
  • 具体体现
    • 新增命令 :当需要添加新的命令时,比如增加一个调节灯光亮度的命令,只需要创建一个新的具体命令类(如LightBrightnessCommand),实现Command接口,而不需要修改现有的Command接口、调用者类(RemoteControl)和接收者类(Light)。
    • 新增接收者 :如果要引入新的设备,如智能窗帘,只需要创建新的接收者类(Curtain)和对应的命令类(如CurtainOpenCommandCurtainCloseCommand),原有的系统结构和代码基本无需改动。

依赖倒置原则(Dependency Inversion Principle)

  • 解释:高层模块不应该依赖低层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。
  • 具体体现
    • 调用者与命令的关系RemoteControl作为高层模块,不直接依赖具体的命令类(如LightOnCommand),而是依赖抽象的Command接口。这样,当具体命令类发生变化时,RemoteControl类不受影响。
    • 具体命令类与接收者的关系 :具体命令类依赖于接收者的抽象(虽然这里接收者没有显式的抽象,但可以理解为依赖接收者的操作接口),而不是具体的接收者实现细节。例如,LightOnCommand只关心Light类有turnOn方法,而不关心Light类内部具体如何实现开启灯光的操作。

迪米特法则(Law of Demeter)

  • 解释:一个对象应该对其他对象有最少的了解,即一个类应该尽量减少与其他类之间的交互,只与直接的朋友通信。
  • 具体体现
    • 调用者与接收者的隔离RemoteControl作为调用者,它只与Command对象交互,不需要了解接收者(如LightAirConditioner)的具体实现。通过命令对象作为中间层,降低了调用者与接收者之间的耦合度。
    • 具体命令类的独立性 :具体命令类(如LightOnCommand)只与它的直接朋友(Light接收者和Command接口)进行交互,不与其他无关的类产生不必要的联系。

命令模式的优缺点

优点

1. 解耦请求发送者和接收者
  • 发送者(如调用者类)不需要知道接收者(如具体执行操作的对象)是谁,也不需要了解接收者的具体实现细节。发送者只需要通过命令对象来间接执行操作,降低了系统的耦合度。例如在智能家居系统中,遥控器(发送者)不需要知道灯具(接收者)的具体电路和控制方式,只需要触发相应的命令对象即可。
2. 可扩展性强
  • 方便添加新的命令,只需要创建新的具体命令类并实现命令接口即可,不会影响到现有的其他类和模块。当系统需要新增功能时,如在智能家居系统中增加调节窗帘开合的功能,只需创建新的命令类和对应的接收者类,而无需修改遥控器(调用者)的代码。
3. 支持命令的撤销和重做
  • 可以在命令对象中记录命令执行前的状态,在需要撤销时恢复到之前的状态,重做时再次执行命令。这在许多应用场景中非常有用,如文本编辑器中的撤销和重做操作,通过命令模式可以很方便地实现。
4. 便于实现命令队列和日志记录
  • 可以将命令对象放入队列中,按照顺序依次执行,实现命令的排队执行。同时,也可以方便地记录命令的执行日志,便于系统的监控和故障排查。例如在多任务处理系统中,可以将一系列命令放入队列中,按顺序依次处理。
5. 符合开闭原则
  • 对扩展开放,对修改关闭。当需要增加新的命令时,只需要扩展新的具体命令类,而不需要修改现有的调用者和其他命令类,提高了系统的可维护性。

缺点

1. 类的数量增加
  • 每个具体的命令都需要创建一个对应的具体命令类,当系统中的命令较多时,会导致类的数量急剧增加,增加了系统的复杂性和维护成本。例如在一个复杂的游戏系统中,各种不同的操作都需要封装成命令类,可能会产生大量的类文件。
2. 命令过多时管理复杂
  • 如果系统中有大量的命令,管理这些命令对象会变得困难。需要有良好的组织和管理机制,否则会导致代码混乱,降低代码的可读性和可维护性。
3. 增加系统的理解难度
  • 由于引入了命令对象、调用者、接收者等多个角色和层次,对于不熟悉命令模式的开发者来说,理解和掌握系统的整体架构和工作流程会有一定的难度。
4. 性能开销
  • 每个命令都封装成一个对象,会带来一定的内存开销和对象创建、销毁的性能开销。特别是在对性能要求较高的系统中,这种开销可能会成为一个问题。

注意事项

设计层面

  1. 合理设计命令接口
    • 命令接口应简洁且具有通用性,只包含必要的方法,如execute()undo()。过多的方法会增加具体命令类的实现复杂度,破坏单一职责原则。
    • 接口的命名要清晰,能准确表达命令的基本操作,方便其他开发者理解和使用。
  2. 控制类的数量
    • 命令模式可能会导致类的数量增多,尤其是在系统功能复杂、命令多样的情况下。要合理规划命令类的层次结构,避免类的泛滥。可以通过抽象命令类或命令组来减少重复代码和类的数量。
    • 例如,对于具有相似操作的命令,可以抽象出一个基类,让具体命令类继承该基类,复用公共的代码逻辑。
  3. 遵循设计原则
    • 严格遵循单一职责原则,确保每个类和接口只负责单一的功能。例如,具体命令类只负责封装特定的操作,接收者类只负责执行具体的业务逻辑。
    • 遵循开闭原则,在需要扩展新命令时,通过创建新的具体命令类来实现,而不是修改现有的代码。这样可以提高系统的可维护性和扩展性。

实现层面

  1. 内存管理
    • 由于命令模式会创建大量的命令对象,要注意内存的使用情况。特别是在命令频繁执行和撤销的场景下,可能会产生大量的临时对象,导致内存占用过高。
    • 可以考虑使用对象池技术来复用命令对象,减少对象的创建和销毁开销,提高系统的性能。
  2. 命令撤销和重做的实现
    • 如果需要支持命令的撤销和重做功能,要确保在命令对象中正确记录执行前的状态。在实现undo()方法时,要保证能准确地恢复到执行命令之前的状态。
    • 同时,要处理好撤销和重做操作的顺序和边界条件,避免出现状态不一致的问题。
  3. 线程安全
    • 在多线程环境下使用命令模式时,要考虑线程安全问题。如果多个线程同时操作命令队列或命令对象,可能会导致数据不一致或其他并发问题。
    • 可以通过同步机制(如synchronized关键字或Lock接口)来保证线程安全,或者使用线程安全的数据结构来存储命令。

使用场景层面

  1. 适用场景判断
    • 命令模式适用于需要将请求的发送者和接收者解耦、支持命令的撤销和重做、实现命令队列和日志记录等场景。在使用前要仔细分析系统需求,判断是否真正适合使用命令模式。
    • 如果系统的功能简单,请求和执行逻辑紧密耦合,使用命令模式可能会增加系统的复杂度,得不偿失。
  2. 与其他模式结合使用
    • 在实际开发中,命令模式可以与其他设计模式结合使用,以更好地满足系统需求。例如,与观察者模式结合,当命令执行完成后通知相关的观察者;与组合模式结合,实现命令的组合和嵌套执行。
    • 但在结合使用时,要注意不同模式之间的协调和兼容性,避免引入新的问题。

应用场景

1. 图形用户界面(GUI)应用程序

  • 按钮和菜单操作 :在各种桌面应用程序、Web 应用程序或移动应用程序的图形用户界面中,用户通过点击按钮、选择菜单项等操作来触发各种功能。每个操作都可以封装成一个命令对象。例如,在文本编辑器中,"保存""复制""粘贴" 等操作对应的按钮点击事件可以分别封装成SaveCommandCopyCommandPasteCommand等命令对象。这样,GUI 组件(如按钮)作为调用者,只需要调用命令对象的execute()方法,而不需要关心具体的操作是如何实现的。
  • 撤销和重做功能 :命令模式天然支持撤销和重做操作。在 GUI 应用中,这是一个非常常见且重要的功能。比如在图像编辑软件中,用户进行的每一步操作(如裁剪、调色、添加滤镜等)都可以封装成命令对象。当用户点击 "撤销" 按钮时,系统可以依次调用已执行命令对象的undo()方法,将图像恢复到之前的状态;点击 "重做" 按钮时,则可以再次调用这些命令对象的execute()方法。

2. 游戏开发

  • 游戏操作控制 :游戏中的角色移动、攻击、释放技能等操作都可以使用命令模式来实现。例如,在一个角色扮演游戏中,玩家按下键盘上的某个按键来控制角色向前移动,这个操作可以封装成MoveForwardCommand命令对象。游戏控制器作为调用者,根据玩家的输入调用相应命令对象的execute()方法。这样可以方便地实现游戏操作的自定义和扩展,例如可以通过修改命令对象来改变角色的移动速度或攻击方式。
  • 游戏回放和存档:命令模式可以记录游戏中的所有操作,这些操作记录可以用于游戏回放。同时,也可以将这些命令序列保存到文件中,实现游戏存档功能。当玩家加载存档时,系统可以依次执行这些命令,将游戏状态恢复到存档时的状态。

3. 工作流系统

  • 任务调度和执行 :在工作流系统中,每个任务的执行可以看作是一个命令。例如,在一个项目管理系统中,任务的分配、审批、完成等操作都可以封装成不同的命令对象。工作流引擎作为调用者,根据工作流的规则依次调用这些命令对象的execute()方法,完成任务的调度和执行。
  • 流程撤销和恢复:如果工作流中某个步骤出现错误或需要修改,命令模式的撤销和重做功能可以方便地实现流程的回退和恢复。例如,在一个审批流程中,如果审批人员误操作通过了一个不应该通过的申请,可以通过撤销相应的审批命令,将流程恢复到之前的状态。

4. 多线程和分布式系统

  • 任务队列和异步处理:在多线程或分布式系统中,命令模式可以用于实现任务队列和异步处理。将需要执行的任务封装成命令对象,放入任务队列中,多个工作线程或分布式节点从队列中取出命令对象并执行。这样可以实现任务的解耦和异步执行,提高系统的并发性能和可扩展性。
  • 日志记录和故障恢复:在分布式系统中,命令模式可以方便地记录每个操作的日志。当系统出现故障时,可以根据日志中的命令序列进行故障恢复。例如,在一个分布式数据库系统中,对数据库的增删改操作可以封装成命令对象,记录操作日志。当数据库节点出现故障重启时,可以根据日志中的命令对象重新执行操作,保证数据的一致性。

5. 智能家居系统

  • 设备控制 :智能家居系统中,用户可以通过手机 APP 或智能遥控器来控制各种智能设备,如灯光、空调、窗帘等。每个设备的操作(如开灯、调节温度、打开窗帘等)都可以封装成命令对象。智能控制中心作为调用者,根据用户的指令调用相应命令对象的execute()方法,实现对设备的远程控制。
  • 场景模式设置:智能家居系统通常支持场景模式,如 "回家模式""睡眠模式" 等。每个场景模式可以看作是一组命令的集合。例如,"回家模式" 可以包含打开灯光、打开空调、拉开窗帘等命令。当用户选择某个场景模式时,系统依次执行该场景模式下的所有命令对象。
相关推荐
多多*7 分钟前
MyBatis-Plus 元对象处理器 @TableField注解 反射动态赋值 实现字段自动填充
java·开发语言·数据库·windows·github·mybatis
Su米苏26 分钟前
SpringBoot敏感数据脱敏怎么处理
java
搬山境KL攻城狮30 分钟前
复杂html动态页面高还原批量导出方案
java·html
程序猿小D31 分钟前
第三百七十二节 JavaFX教程 - JavaFX HTMLEditor
java·服务器·前端
B站计算机毕业设计超人36 分钟前
计算机毕业设计SpringBoot+Vue.js社区智慧养老监护管理平台(源码+文档+PPT+讲解)
java·vue.js·spring boot·后端·毕业设计·课程设计·毕设
勿忘初心122137 分钟前
idea生成自定义Maven原型(archetype)项目工程模板
java·maven·intellij-idea
B站计算机毕业设计超人38 分钟前
计算机毕业设计SpringBoot+Vue.js在线问卷调查系统(源码+文档+PPT+讲解)
java·vue.js·spring boot·后端·毕业设计·课程设计·毕设
李洛克071 小时前
openssl下aes128算法ofb模式加解密运算实例
开发语言·c++·算法
java12241 小时前
各种传参形式
java·状态模式
码有余悸1 小时前
Java 连接 Redis 的两种方式
java·redis·bootstrap