什么是命令模式(Command Pattern)

命令模式将一个请求封装为一个对象,从而允许你使用不同的请求、队列或日志来参数化其他对象。命令模式也支持可撤销的操作。
想象一下你去餐厅点餐:
  • 你(Client)决定要点什么菜(请求)。
  • 你告诉服务员(Invoker)你的选择。
  • 服务员写下订单(Command 对象),订单上包含了菜品信息(请求细节)以及由谁来做(隐式指向厨师 Receiver)。服务员不需要知道怎么做菜。
  • 服务员把订单交给后厨(传递 Command)。
  • 厨师(Receiver)拿到订单,根据订单内容烹饪菜肴(执行请求)。
在这个过程中,“订单”就是一个命令对象,它解耦了顾客(发出请求)和服务员(触发执行)与厨师(实际执行者)之间的直接联系。服务员只需要知道“执行这个订单”,而不需要关心订单的具体内容和执行细节。
命令模式的核心在于将动作(请求)本身对象化。

核心思想/步骤

  1. 定义命令接口 (Command): 定义一个接口,通常包含一个 execute() 方法。所有具体的命令类都需要实现这个接口。有时还会包含一个 undo() 方法来支持撤销。
  1. 创建具体命令 (ConcreteCommand): 实现 Command 接口。它内部通常包含一个对接收者 (Receiver) 对象的引用。execute() 方法的实现会调用 Receiver 对象上的一个或多个操作来完成请求。它也可以存储执行命令所需的参数。
  1. 定义接收者 (Receiver): 包含执行请求相关的具体业务逻辑。它知道如何执行与请求相关的操作。一个命令对象只对应一个接收者,但一个接收者可以处理多个命令。
  1. 创建调用者 (Invoker): 持有一个或多个 Command 对象。它不了解命令的具体内容或接收者是谁。当需要执行命令时,调用者只调用 Command 对象的 execute() 方法。调用者可以是按钮、菜单项、或者一个任务队列。
  1. 客户端 (Client): 负责创建 ConcreteCommand 对象,并设置其 Receiver。然后,将这个 Command 对象配置给 Invoker。

优点

  1. 解耦调用者和接收者 (Decoupling): 调用者(Invoker)和接收者(Receiver)之间完全解耦。调用者只需要知道如何调用 execute(),不需要知道请求的具体内容和由谁执行。
  1. 易于扩展新命令 (Extensibility): 添加新的命令非常简单,只需要创建一个新的 ConcreteCommand 类实现 Command 接口即可,无需修改现有代码(符合开闭原则)。
  1. 命令可以组合 (Composability): 可以将多个命令组合成一个复合命令(Macro Command),实现一系列操作的批处理。
  1. 支持撤销和重做 (Undo/Redo): 通过在 Command 接口中添加 undo() 方法,并在 ConcreteCommand 中实现相应的反向操作,可以轻松实现撤销和重做功能(通常需要维护一个命令历史栈)。
  1. 支持队列和日志 (Queueing & Logging): 命令对象可以被存储在队列中,用于异步执行、任务调度或失败重试。命令的执行过程也可以被记录下来。
  1. 将操作参数化: 可以将命令对象作为参数传递给方法,实现回调或在运行时动态指定行为。

可能的缺点

  1. 类的数量膨胀: 每个具体的操作都需要一个对应的 ConcreteCommand 类,如果系统中有大量操作,可能会导致类的数量急剧增加。
  1. 理解成本: 对于非常简单的操作,引入命令模式可能会增加系统的复杂性,使得代码理解起来稍微困难一些。

使用场景

  1. 当需要将请求的发送者接收者解耦时。
  1. 当需要将操作参数化,以便在不同的时间点指定、排队或执行请求时。
  1. 当需要支持撤销 (Undo)重做 (Redo) 或事务(一系列操作要么全成功要么全失败)功能时。
  1. GUI 应用程序: 按钮点击、菜单项选择等触发的操作,可以将每个操作封装成一个 Command 对象,由 UI 元素(Invoker)触发。
  1. 任务队列系统: 将待执行的任务(如下载、数据处理)封装成 Command 对象放入队列。
  1. 宏录制与回放: 将用户的操作序列记录为 Command 对象列表,然后按顺序执行以实现回放。
  1. Android 中的场景:
      • 实现撤销/重做: 在文本编辑器、绘图应用或任何需要撤销操作的场景中,将用户的每次修改(输入文字、删除、绘制图形)封装为 Command 对象。
      • 网络请求队列: 将每个 API 请求封装成一个 Command 对象,放入队列中按顺序或并发执行,方便管理(如添加重试逻辑)。
      • 遥控器应用: 手机 App 作为遥控器,每个按钮(如“开灯”、“关灯”、“调高音量”)对应一个 Command 对象,发送给接收设备(Receiver,如智能灯泡、电视)。
      • 复杂操作的封装: 将一个涉及多个步骤或多个对象交互的复杂操作封装在一个 Command 对象中,简化调用者的逻辑。
      • 统一处理来自不同触发点的相同操作: 比如一个“保存”操作可能由菜单项、工具栏按钮或快捷键触发,它们都可以关联到同一个 SaveCommand 对象。

UML 图

下面是基于一个简单的“控制电灯开关”示例的命令模式 UML 图:
  • Command (Interface): 定义了 execute() 和 undo() 方法。
  • Light (Receiver): 知道如何开关灯 (turnOn(), turnOff())。
  • LightOnCommand, LightOffCommand (ConcreteCommand): 实现了 Command 接口。它们都持有一个 Light 对象的引用。execute() 调用 light.turnOn() 或 light.turnOff(),undo() 则执行相反的操作。
  • RemoteControl (Invoker): 持有一个 Command 对象 (slot)。buttonWasPressed() 调用 slot.execute()。它还维护了一个 undoCommand 来支持简单的撤销。
  • Client: 创建 Light、LightOnCommand、LightOffCommand 和 RemoteControl 实例,并将命令设置到遥控器上。

代码示例

在这个例子中:
  • Light 是接收者,负责实际开关灯。
  • LightOnCommand 和 LightOffCommand 是具体命令,封装了开灯和关灯的请求,并知道如何通过调用 Light 的方法来执行和撤销。
  • RemoteControl 是调用者,它只有一个按钮 (buttonWasPressed) 和一个撤销按钮 (undoButtonWasPressed)。它通过 setCommand 方法被配置,然后调用命令的 execute 或 undo。
  • main 函数是客户端,负责创建所有对象并将它们连接起来。
  • 我们还引入了一个 NoCommand(空对象模式的应用),用于表示没有命令被设置的情况,避免空指针检查。

总结

  • 命令模式通过将请求封装成对象,极大地增强了系统的灵活性和可扩展性
  • 解耦了请求的发送者和接收者,使得可以独立地改变它们。
  • 支持队列、日志、撤销/重做等高级功能是其主要优势。
  • 在 Android 开发中,对于需要管理操作序列、实现撤销功能或解耦 UI 事件与业务逻辑的场景非常有用。
💡
记住它的核心:请求变对象,调用者只管执行,接收者负责实现
 
设计模式——行为型-中介者模式设计模式——行为型-状态模式
Loading...