文章目录
一、引言
命令模式是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。其实现有些烦琐,适用于一些比较专用的场合。
二、命令模式
命令模式就是将请求转换为一个包含与请求相关的所有信息的独立对象,通过这个转换能够让使用者根据不同的请求将客户参数化、 延迟请求执行或将请求放入队列中或记录请求日志, 且能实现可撤销操作。
我们设想去食堂吃饭,买了一份红烧鱼和锅包肉。设计相关代码:
c++
class Cook {
public:
//做红烧鱼
void cook_fish()
{
cout << "做一盘红烧鱼菜品" << endl;
}
//做锅包肉
void cook_meat()
{
cout << "做一盘锅包肉菜品" << endl;
}
//做其他各种菜品
private:
};
由于食堂没有服务员,只有厨师,所以我们需要直接把想吃的东西报给食堂阿姨,阿姨给我们做饭:
c++
Cook* cook = new Cook();
cook->cook_fish();
cook->cook_meat();
delete cook;
试想一下,如果饭馆里来的顾客比较多,每位顾客都直接把菜品报给厨师,那么厨师就容易记错而产生混乱,所以需要引人一个服务员(一个新类),服务员给每位顾客一个便签,顾客把需要的菜品写在便签上,然后服务员把便签拿给厨师,厨师依据收到的便签顺序以及每个便签上标记的菜品依次做菜,这样,饭馆的点菜步骤就显得井然有序。相关的实现代码采取的就是命令模式。
首先,将厨师能做的每样菜(Cook
类中的每个成员函数)都看成一个命令(顾客所写的便签中的某个菜品),先创建命令对应的抽象父类Command
(抽象菜单),代码如下:
c++
//可做菜品的抽象类
class Command {
public:
Command(Cook*cok):_cook(cok){}
virtual void Execute() = 0;
virtual ~Command(){}
protected:
Cook* _cook;
};
由上面的代码可以看到,Execute
是一个纯虚函数,子类要在其中实现厨师制作某个菜品(执行某个命令)的代码,在命令模式中习惯这样命名,当然不叫Execute
而叫其他名字也完全可以。有了Command
抽象类,就可以针对厨师能做的每样菜都实现一个具体的命令子类:
c++
// 红烧鱼命令类
class CommandFish : public Command {
public:
CommandFish(Cook* cook) : Command(cook) {}
void Execute() override {
_cook->cook_fish();
}
};
// 锅包肉命令类
class CommandMeat : public Command {
public:
CommandMeat(Cook* cook) : Command(cook) {}
void Execute() override {
_cook->cook_meat();
}
~CommandMeat() {}
};
以上实现了两个继承自Command
的子类CommandFish
和CommandMeat
,当然,厨师如果还做其他菜品,则应该继续创建其他Command
子类。值得注意的是,每个子类的Execute
调用的其实还是厨师类中对应的制作相关菜品的成员函数。这里有一个有意思的事,例如创建一个CommandMeat
对象(命令对象),该对象意味着让厨师做一盘锅包肉菜品这样一个动作,显然是将一个动作封装成了一个对象,该对象可以通过诸如函数参数等方式进行传递,将一个动作当作参数进行传递。
c++
Cook cook;
Command* pcmd1 = new CommandFish( & cook);
pcmd1->Execute();//做红烧鱼
Command* pcmd2 = new CommandMeat(& cook);
pcmd2->Execute();//做锅包肉
//释放资源
delete pcmd1;
delete pcmd2;
上述代码相当于顾客在每个便签上点了一个菜品,然后将便签交到厨师手里让厨师开始做菜,这样做有个问题,如果要点10个不同的菜品,那就要new
10次Command
子类对象,很不方便,也就是说,顾客点10个菜没必要写10张便签并逐一拿给厨师,只需要在一张便签上写下10个菜名即可,为了支持这种点菜的便利性,有必要引人一个可以与顾客直接接触的服务员类,顾客只需要在一张便签上写下10个菜品的名字,直接拿给服务员,服务员替顾客下单让厨师做菜。现在先来实现初步的服务员类:
c++
class Waiter
{
public:
void AddCommand(Command* pcommand)
{
_commands.push_back(pcommand);
}
void DelCommand(Command* pcommand)
{
_commands.remove(pcommand);
}
void Notify()
{
for (auto command : _commands)
command->Execute();
}
private:
list<Command*> _commands;
};
这样我们就可以实现一次点多个菜:
c++
Cook cook;
Waiter waiter;
waiter.AddCommand(new CommandFish(&cook));
waiter.AddCommand(new CommandMeat(&cook));
// 执行订单中的命令
waiter.Notify();
命令模式一般包含5种角色:
-
接收者类 (Receiver ):知道如何实施与执行一个请求相关的操作。这里指
Cook
类。提供了对请求的业务进行处理的接口(cook_fish
、cook_meat
)。 -
调用者类 (Invoker ):请求的发送者,通过命令对象来执行请求。这里指
Waiter
类。该类只与抽象命令类(Command)之间存在关联关系。 -
抽象命令类 (Command ):声明执行操作的接口。这里指的正是
Command
类。在其中声明了用于执行请求的Execute方法(成员函数),用于调用接收者类中的相关操作。 -
具体命令类 (ConcreteCommand ):抽象命令类的子类。这里指的是
CommandFish
类和
CommandMeat
类。类中实现了执行请求的Execute
方法来调用接收者类Cook
中的相关操作(
cook_fish
和cook_meat
方法)。 -
客户端 (Client):创建具体的命令类对象并设定它的接收者,即main函数的内容。
调用者Waiter
和接收者Cook
是解耦的,也就是说,发出请求的对象和接收请求的对象是解耦的,服务员并不关心厨师怎样做菜,只是把顾客的便签交到厨师手中(调用Notify
成员函数),和厨师沟通的实际是便签。而顾客的点菜便签就是命令对象,也就是Command
类对象,用来请求厨师做菜,该对象可以被当作参数从顾客手中传递到服务员手中,再从服务员手中传递到厨师手中,Command
类只包含一个Execute
接口(成员函数),该接口封装了做菜所需的动作。
服务员不关心便签中的内容,只需要接下顾客的便签,当便签累积到一定数量后,调用Notify
成员函数将便签交到厨师手中即可。当然,服务员需要知道便签中有Execute
方法(所有便签都支持该方法),因为在Waiter
类的Notify
中需要调用便签中的Execute
方法。
命令模式结构
引人命令设计模式的定义 (实现意图):将一个请求或者命令(做红烧鱼或做锅包肉)封装为一个对象(CommandFish
类对象或CommandMeat
类对象),以便这些请求可以以对象的方式通过参数进行传递(参数化:调用者不关心具体是什么命令对象,只把这些对象当作参数一样看待),对象化了的请求还可以排队执行或者根据需要将这些请求录人日志供查看和排错,以及支持请求执行后的可撤销操作。命令在发送者和请求者之间建立单向连接。
三、总结
命令模式最核心的实现手段,就是将对成员函数的调用封装成命令对象,也就是对请求进行封装,命令对象将动作和接收者包裹到了对象中并且只暴露出了一个Execute
方法以让接收者执行动作。想一下C语言中支持的函数指针可以把函数当作变量传来传去。但是,在许多编程语言中,函数无法作为参数传递给其他函数,也无法赋值给其他变量。借助命令模式,可以将函数(Cook
类中的cook_fish
和cook_meat
)封装成类(CommandFish
和CommandMeat
)对象,实现把函数像对象一样使用月------对象使用起来非常灵活,例如通过函数参数来传递、存储、序列化等。
当把函数调用封装成对象之后会更加便于存储和传播,也更加便于控制。所以,诸如异步执行、延迟执行、排队执行、撤销、执行过程中增加日志记录等,都是命令模式能发挥独特作用的地方,是命令模式的主要应用场景。
我们在使用命令模式的时候,可以这样做:
- 声明仅有一个执行方法的命令接口。
- 抽取请求并使之成为实现命令接口的具体命令类。 每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。 所有这些变量的数值都必须通过命令构造函数进行初始化。
- 找到担任发送者职责的类。 在这些类中添加保存命令的成员变量。 发送者只能通过命令接口与其命令进行交互。 发送者自身通常并不创建命令对象, 而是通过客户端代码获取。
- 修改发送者使其执行命令, 而非直接将请求发送给接收者。
- 客户端必须按照以下顺序来初始化对象:
- 创建接收者。
- 创建命令, 如有需要可将其关联至接收者。
- 创建发送者并将其与特定命令关联。
该设计模式遵循了单一职责原则和开闭原则。可以解耦触发和执行操作的类,可以在不修改已有客户端代码的情况下在程序中创建新的命令。可以实现撤销和恢复功能。可以实现操作的延迟执行。同时也可以将一组简单命令组合成一个复杂命令。
命令和策略模式看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。
- 可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。
- 另一方面, 策略通常可用于描述完成某件事的不同方式, 能够在同一个上下文类中切换算法。
我们可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。当然,也可以使用原型模式可用于保存命令的历史记录。可以同时使用命令和备忘录模式来实现 "撤销"。 在这种情况下,命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。
策略通常可用于描述完成某件事的不同方式, 能够在同一个上下文类中切换算法。
我们可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。当然,也可以使用原型模式可用于保存命令的历史记录。可以同时使用命令和备忘录模式来实现 "撤销"。 在这种情况下,命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。