跳转至

命令模式

命令模式是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。

模式结构

  1. 发送者 (Sender) 亦称 “触发者 (Invoker)” 类负责对请求进行初始化, 其中必须包含一个成员变量来存储对于命令对象的引用。 发送者触发命令, 而不向接收者直接发送请求。 注意, 发送者并不负责创建命令对象: 它通常会通过构造函数从客户端处获得预先生成的命令。
  2. 命令 (Command) 接口通常仅声明一个执行命令的方法。
  3. 具体命令 (Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务逻辑对象。 但为了简化代码, 这些类可以进行合并。

    接收对象执行方法所需的参数可以声明为具体命令的成员变量。 你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化。

  4. 接收者 (Receiver) 类包含部分业务逻辑。 几乎任何对象都可以作为接收者。 绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作。

  5. 客户端 (Client) 会创建并配置具体命令对象。 客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。 此后, 生成的命令就可以与一个或多个发送者相关联了。

应用场景

  • 如果你需要通过操作来参数化对象, 可使用命令模式。

    命令模式可将特定的方法调用转化为独立对象。 这一改变也带来了许多有趣的应用: 你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。

    举个例子: 你正在开发一个 GUI 组件 (例如上下文菜单), 你希望用户能够配置菜单项, 并在点击菜单项时触发操作。

  • 如果你想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式。

    同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。

  • 如果你想要实现操作回滚功能, 可使用命令模式。

    尽管有很多方法可以实现撤销和恢复功能, 但命令模式可能是其中最常用的一种。

    为了能够回滚操作, 你需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。

    这种方法有两个缺点。 首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。 你可以使用备忘录模式来在一定程度上解决这个问题。

    其次, 备份状态可能会占用大量内存。 因此, 有时你需要借助另一种实现方式: 命令无需恢复原始状态, 而是执行反向操作。 反向操作也有代价: 它可能会很难甚至是无法实现。

实现方式

下面我们通过电视机的例子来了解命令模式。 你可通过以下方式打开电视机:

  • 按下遥控器上的 ON 开关;
  • 按下电视机上的 ON 开关。

我们可以从实现 ON 命令对象并以电视机作为接收者入手。 当在此命令上调用 execute 执行方法时, 方法会调用 TV.on 打开电视函数。 最后的工作是定义请求者: 这里实际上有两个请求者: 遥控器和电视机。 两者都将嵌入 ON 命令对象。

注意我们是如何将相同请求封装进多个请求者的。 我们也可以采用相同的方式来处理其他命令。 创建独立命令对象的优势在于可将 UI 逻辑与底层业务逻辑解耦。 这样就无需为每个请求者开发不同的处理者了。 命令对象中包含执行所需的全部信息, 所以也可用于延迟执行。

package main

type Device interface {
    on()
    off()
}
package main

import "fmt"

type Tv struct {
    isRunning bool
}

func (t *Tv) on() {
    t.isRunning = true
    fmt.Println("Turning tv on")
}

func (t *Tv) off() {
    t.isRunning = false
    fmt.Println("Turning tv off")
}
  1. 声明仅有一个执行方法的命令接口。
package main

type Command interface {
    execute()
}
  1. 抽取请求并使之成为实现命令接口的具体命令类。 每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。 所有这些变量的数值都必须通过命令构造函数进行初始化。
package main

type OnCommand struct {
    device Device
}

func (c *OnCommand) execute() {
    c.device.on()
}
package main

type OffCommand struct {
    device Device
}

func (c *OffCommand) execute() {
    c.device.off()
}
  1. 找到担任发送者职责的类。 在这些类中添加保存命令的成员变量。 发送者只能通过命令接口与其命令进行交互。 发送者自身通常并不创建命令对象, 而是通过客户端代码获取。
  2. 修改发送者使其执行命令, 而非直接将请求发送给接收者。
package main

type Button struct {
    command Command
}

func (b *Button) press() {
    b.command.execute()
}
  1. 客户端必须按照以下顺序来初始化对象:
    • 创建接收者。
    • 创建命令, 如有需要可将其关联至接收者。
    • 创建发送者并将其与特定命令关联。
package main

func main() {
    tv := &Tv{}

    onCommand := &OnCommand{
        device: tv,
    }

    offCommand := &OffCommand{
        device: tv,
    }

    onButton := &Button{
        command: onCommand,
    }
    onButton.press()

    offButton := &Button{
        command: offCommand,
    }
    offButton.press()
}
Turning tv on
Turning tv off

优缺点

优点 缺点
单一职责原则。 你可以解耦触发和执行操作的类。 代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次。
开闭原则。 你可以在不修改已有客户端代码的情况下在程序中创建新的命令。
你可以实现撤销和恢复功能。
你可以实现操作的延迟执行。
你可以将一组简单命令组合成一个复杂命令。

与其他模式的关系

  • 责任链模式命令模式中介者模式观察者模式 用于处理请求发送者和接收者之间的不同连接方式:
  • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
  • 命令在发送者和请求者之间建立单向连接。
  • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
  • 观察者允许接收者动态地订阅或取消接收请求。
  • 责任链 的管理者可使用 命令模式 实现。 在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。

    还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。

  • 你可以同时使用 命令备忘录模式 来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。

  • 命令策略模式 看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。

  • 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。

  • 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。

  • 原型模式 可用于保存命令的历史记录。

  • 你可以将 访问者模式 视为 命令模式 的加强版本, 其对象可对不同类的多种对象执行操作。