假如你正在开发一款新的文字编辑器,当前的任务是创建一个包含多个按钮的工具栏,并让每个按钮对应编辑器的不同操作。你创建了一个非常简洁的按钮
类,它不仅可用于生成工具栏上的按钮,还可用于生成各种对话框的通用按钮。
尽管所有按钮看上去都很相似,但它们可以完成不同的操作(打开、保存、打印和应用等)。你会在哪里放置这些按钮的点击处理代码呢?最简单的解决方案是在使用按钮的每个地方都创建大量的子类,这些子类中包含按钮点击后必须执行的代码。
你很快就意识到这种方式有严重缺陷。首先,你创建了大量的子类,当每次修改基类按钮
时,你都有可能需要修改所有子类的代码。简单来说,GUI 代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码。
还有一个部分最难办。复制 / 粘贴文字等操作可能会在多个地方被调用。例如用户可以点击工具栏上小小的 “复制” 按钮,或者通过上下文菜单复制一些内容,又或者直接使用键盘上的Ctrl+C
。
我们的程序最初只有工具栏,因此可以使用按钮子类来实现各种不同操作。换句话来说,复制按钮CopyButton
子类包含复制文字的代码是可行的。在实现了上下文菜单、快捷方式和其他功能后,你要么需要将操作代码复制进许多个类中,要么需要让菜单依赖于按钮,而后者是更糟糕的选择。
解决方案:
优秀的软件设计通常会将关注点进行分离,而这往往会导致软件的分层。最常见的例子:一层负责用户图像界面; 另一层负责业务逻辑。GUI 层负责在屏幕上渲染美观的图形,捕获所有输入并显示用户和程序工作的结果。当需要完成一些重要内容时(比如计算月球轨道或撰写年度报告),GUI 层则会将工作委派给业务逻辑底层。
这在代码中看上去就像这样:一个 GUI 对象传递一些参数来调用一个业务逻辑对象。这个过程通常被描述为一个对象发送请求给另一个对象。
命令模式建议 GUI 对象不直接提交这些请求。你应该将请求的所有细节(例如调用的对象、方法名称和参数列表) 抽取出来组成命令类,该类中仅包含一个用于触发请求的方法。
命令对象负责连接不同的 GUI 和业务逻辑对象。此后,GUI 对象无需了解业务逻辑对象是否获得了请求,也无需了解其对请求进行处理的方式。GUI 对象触发命令即可,命令对象会自行处理所有细节工作。
下一步是让所有命令实现相同的接口。该接口通常只有一个没有任何参数的执行方法,让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令。此外还有额外的好处,现在你能在运行时切换连接至发送者的命令对象,以此改变发送者的行为。
你可能会注意到遗漏的一块拼图——请求的参数。GUI 对象可以给业务层对象提供一些参数。但执行命令方法没有任何参数,所以我们如何将请求的详情发送给接收者呢?答案是:使用数据对命令进行预先配置,或者让其能够自行获取数据。
让我们回到文本编辑器。应用命令模式后,我们不再需要任何按钮子类来实现点击行为。我们只需在按钮Button
基类中添加一个成员变量来存储对于命令对象的引用,并在点击后执行该命令即可。
你需要为每个可能的操作实现一系列命令类,并且根据按钮所需行为将命令和按钮连接起来。
其他菜单、快捷方式或整个对话框等 GUI 元素都可以通过相同方式来实现。当用户与 GUI 元素交互时,与其连接的命令将会被执行。现在你很可能已经猜到了,与相同操作相关的元素将会被连接到相同的命令,从而避免了重复代码。
最后,命令成为了减少 GUI 和业务逻辑层之间耦合的中间层。而这仅仅是命令模式所提供的一小部分好处!
真实世界类比:
在市中心逛了很久的街后,你找到了一家不错的餐厅,坐在了临窗的座位上。一名友善的服务员走近你,迅速记下你点的食物,写在一张纸上。服务员来到厨房,把订单贴在墙上。过了一段时间,厨师拿到了订单,他根据订单来准备食物。厨师将做好的食物和订单一起放在托盘上。服务员看到托盘后对订单进行检查,确保所有食物都是你要的,然后将食物放到了你的桌上。
那张纸就是一个命令,它在厨师开始烹饪前一直位于队列中。命令中包含与烹饪这些食物相关的所有信息。厨师能够根据它马上开始烹饪,而无需跑来直接和你确认订单详情。
(1)模式动机
在软件构建过程中,“行为请求者” 与 “行为实现者” 通常呈现一种 “紧耦合”。但在某些场合——比如需要对行为进行 “记录、撤销/重(undolredo)、事务” 等处理,这种无法抵御变化的紧耦合是不合适的。
在这种情况下,如何将 “行为请求者” 与 “行为实现者” 解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
(2)模式定义
将一个请求(行为)封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
(3)要点总结
a). Command模式的根本目的在于将 “行为请求者” 与 “行为实现者” 解耦,在面向对象语言中,常见的实现手段是 “将行为抽象为对象”。
b). 实现Command接口的具体命令对象ConcreteCommand有时候根据需要可能会保存一些额外的状态信息。通过使用Composite模式,可以将多个 “命令” 封装为一个 “复合命令” MacroCommand.
c). Command模式与C++中的函数对象有些类似。但两者定义行为接口的规范有所区别:Command以面向对象中的 “接口-实现” 来定义行为接口规范,更严格,但有性能损失;C++函数对象以函数签名来定义行为接口规范,更灵活,性能更高。
下面是命令模式的类图结构以及代码,该模式是运行时编译的(如下列代码的execute
方法),利用的是接口实现的如execute
这个接口。而函数对象利用了C++的特征 “泛型编程” 与 “重载括号” 实现了编译时技术,在运行时直接调用就行。
class Command{
public:
virtual void execute() = 0;
};
class ConcreteCommand1 : public Command{
string arg;
public:
ConcreteCommand1(const string &s) : arg(s) {}
void execute() override {
cout << "#1 process..." << arg << endl;
}
};
class ConcreteCommand2 : public Command{
string arg;
public:
ConcreteCommand2(const string &s) : arg(s) {}
void execute() override {
cout << "#2 process..." << arg << endl;
}
};
class MacroCommand : public Command{ //多个命令组合再分发
vector<Command*> commands;
public:
void addCommand(Command *c) { commands.push_back(c); }
void execute() override{
for (auto &c : commands)
c->execute();
}
};
int main(){
ConcreteCommand1 command1(receiver, "Arg ###");
ConcreteCommand2 command2(receiver, "Arg $$$");
MacroCommand macro;
macro.addCommand(&command1);
macro.addCommand(&command2);
macro.execute();
}