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

问题¶
假如你正在开发一款新的文字编辑器, 当前的任务是创建一个包含多个按钮的工具栏, 并让每个按钮对应编辑器的不同操作。 你创建了一个非常简洁的 按钮类, 它不仅可用于生成工具栏上的按钮, 还可用于生成各种对话框的通用按钮。

尽管所有按钮看上去都很相似, 但它们可以完成不同的操作 (打开、 保存、 打印和应用等)。你会在哪里放置这些按钮的点击处理代码呢? 最简单的解决方案是在使用按钮的每个地方都创建大量的子类。 这些子类中包含按钮点击后必须执行的代码。

你很快就意识到这种方式有严重缺陷。 首先, 你创建了大量的子类, 当每次修改基类 按钮时, 你都有可能需要修改所有子类的代码。 简单来说, GUI 代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码。

还有一个部分最难办。 复制/粘贴文字等操作可能会在多个地方被调用。 例如用户可以点击工具栏上小小的 “复制” 按钮, 或者通过上下文菜单复制一些内容, 又或者直接使用键盘上的Ctrl+C 。
我们的程序最初只有工具栏, 因此可以使用按钮子类来实现各种不同操作。 换句话来说, 复制按钮CopyButton子类包含复制文字的代码是可行的。 在实现了上下文菜单、 快捷方式和其他功能后, 你要么需要将操作代码复制进许多个类中, 要么需要让菜单依赖于按钮, 而后者是更糟糕的选择。
解决方案¶
优秀的软件设计通常会将关注点进行分离, 而这往往会导致软件的分层。 最常见的例子: 一层负责用户图像界面; 另一层负责业务逻辑。 GUI 层负责在屏幕上渲染美观的图形, 捕获所有输入并显示用户和程序工作的结果。 当需要完成一些重要内容时 (比如计算月球轨道或撰写年度报告), GUI 层则会将工作委派给业务逻辑底层。
这在代码中看上去就像这样: 一个 GUI 对象传递一些参数来调用一个业务逻辑对象。 这个过程通常被描述为一个对象发送请求给另一个对象。

命令模式建议 GUI 对象不直接提交这些请求。 你应该将请求的所有细节 (例如调用的对象、方法名称和参数列表) 抽取出来组成命令类, 该类中仅包含一个用于触发请求的方法。
命令对象负责连接不同的 GUI 和业务逻辑对象。 此后, GUI 对象无需了解业务逻辑对象是否获得了请求, 也无需了解其对请求进行处理的方式。 GUI 对象触发命令即可, 命令对象会自行处理所有细节工作。

下一步是让所有命令实现相同的接口。 该接口通常只有一个没有任何参数的执行方法, 让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令。 此外还有额外的好处,现在你能在运行时切换连接至发送者的命令对象, 以此改变发送者的行为。
你可能会注意到遗漏的一块拼图——请求的参数。 GUI 对象可以给业务层对象提供一些参数。但执行命令方法没有任何参数, 所以我们如何将请求的详情发送给接收者呢? 答案是: 使用数据对命令进行预先配置, 或者让其能够自行获取数据。

让我们回到文本编辑器。 应用命令模式后, 我们不再需要任何按钮子类来实现点击行为。 我们只需在 按钮Button基类中添加一个成员变量来存储对于命令对象的引用, 并在点击后执行该命令即可。
你需要为每个可能的操作实现一系列命令类, 并且根据按钮所需行为将命令和按钮连接起来。
其他菜单、 快捷方式或整个对话框等 GUI 元素都可以通过相同方式来实现。 当用户与 GUI 元素交互时, 与其连接的命令将会被执行。 现在你很可能已经猜到了, 与相同操作相关的元素将会被连接到相同的命令, 从而避免了重复代码。
最后, 命令成为了减少 GUI 和业务逻辑层之间耦合的中间层。 而这仅仅是命令模式所提供的一小部分好处!
命令模式结构¶

- 发送者 (Sender)——亦称 “触发者 (Invoker)”——类负责对请求进行初始化, 其中必须包含一个成员变量来存储对于命令对象的引用。 发送者触发命令, 而不向接收者直接发送请求。 注意, 发送者并不负责创建命令对象: 它通常会通过构造函数从客户端处获得预先生成的命令。
- 命令 (Command) 接口通常仅声明一个执行命令的方法。
- 具体命令 (Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务逻辑对象。但为了简化代码, 这些类可以进行合并。
- 接收对象执行方法所需的参数可以声明为具体命令的成员变量。 你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化。
- 接收者 (Receiver) 类包含部分业务逻辑。 几乎任何对象都可以作为接收者。 绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作。
- 客户端 (Client) 会创建并配置具体命令对象。 客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。此后, 生成的命令就可以与一个或多个发送者相关联了。
真实世界类比¶

在市中心逛了很久的街后, 你找到了一家不错的餐厅, 坐在了临窗的座位上。 一名友善的服务员走近你, 迅速记下你点的食物, 写在一张纸上。 服务员来到厨房, 把订单贴在墙上。 过了一段时间, 厨师拿到了订单, 他根据订单来准备食物。 厨师将做好的食物和订单一起放在托盘上。 服务员看到托盘后对订单进行检查, 确保所有食物都是你要的, 然后将食物放到了你的桌上。
那张纸就是一个命令, 它在厨师开始烹饪前一直位于队列中。 命令中包含与烹饪这些食物相关的所有信息。 厨师能够根据它马上开始烹饪, 而无需跑来直接和你确认订单详情。
代码示例¶
class Receiver
{
public:
void f()
{
std::cout << 1 << std::endl;
}
};
class Command
{
public:
virtual void execute() = 0;
virtual ~Command() = default;
};
template <typename T>
class CommandA : public Command
{
public:
using Action = void (T::*)();
CommandA(const std::shared_ptr<T> &p, Action a) : receiver(p), action(a) {}
void execute() override
{
std::cout << "A" << std::endl;
if (const auto p = receiver.lock())
{
(p.get()->*action)();
}
}
private:
std::weak_ptr<T> receiver;
Action action;
};
template <typename T>
class CommandB : public Command
{
public:
using Action = void (T::*)();
CommandB(const std::shared_ptr<T> &p, Action a) : receiver(p), action(a) {}
void execute() override
{
std::cout << "B" << std::endl;
if (const auto p = receiver.lock())
{
(p.get()->*action)();
}
}
private:
std::weak_ptr<T> receiver;
Action action;
};
class Invoker : public Command
{
public:
void add(const std::shared_ptr<Command> &c)
{
commands.emplace_back(c);
}
void remove(const std::shared_ptr<Command> &c)
{
commands.remove_if([&](std::weak_ptr<Command> &x) { return x.lock() == c; });
}
void execute() override
{
for (auto &&x : commands)
{
if (const auto p = x.lock())
p->execute();
}
}
private:
std::list<std::weak_ptr<Command>> commands;
};
int main()
{
auto receiver = std::make_shared<Receiver>();
const std::shared_ptr<Command> commandA = std::make_shared<CommandA<Receiver>>(receiver, &Receiver::f);
const std::shared_ptr<Command> commandB = std::make_shared<CommandB<Receiver>>(receiver, &Receiver::f);
Invoker invoker;
invoker.add(commandA);
invoker.add(commandB);
invoker.execute(); //@ A1B1
}
命令模式总结¶
实现方式¶
- 声明仅有一个执行方法的命令接口。
- 抽取请求并使之成为实现命令接口的具体命令类。 每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。 所有这些变量的数值都必须通过命令构造函数进行初始化。
- 找到担任发送者职责的类。 在这些类中添加保存命令的成员变量。 发送者只能通过命令接口与其命令进行交互。 发送者自身通常并不创建命令对象, 而是通过客户端代码获取。
- 修改发送者使其执行命令, 而非直接将请求发送给接收者。
- 客户端必须按照以下顺序来初始化对象:
- 创建接收者。
- 创建命令, 如有需要可将其关联至接收者。
- 创建发送者并将其与特定命令关联。
优点¶
- 单一职责原则。 可以解耦触发和执行操作的类。
- 开闭原则。 可以在不修改已有客户端代码的情况下在程序中创建新的命令。
- 可以实现撤销和恢复功能。
- 可以实现操作的延迟执行。
- 可以将一组简单命令组合成一个复杂命令。
缺点¶
- 代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次。
适用场景¶
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起形成宏命令。