命令模式

命令模式

定义

将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请 求排队或者记录请求日志,可以提供命令的撤销和恢复功能

粗略理解

​ 奇怪,我对于这个命令模式就是有点疑惑,我就是不知道它到底是一个什么情况。让我简单先敲一遍,兴许就能理解问题了。

​ 先举一种简单的现实生活中的命令模式来进行理解。就是餐馆的一个点餐系统。在现实生活中,你需要去思考你到底要选择那一道菜,然后你需要去考虑把你选择的这些个菜单送给服务员,然后服务员会将这些菜送给后厨,然后用户就只需要等待。

​ 但是我们有没有考虑过没有服务员这种情况呢,如果没有服务员,我们就需要去直接与后厨进行对接。这想想都不现实对吧。在程序设计中,这其实也是一种哲学。当我们程序设计得在用户实现一个功能直接调用底层的api时,这种设计是极其糟糕的。当然我这句话的描述有点问题,主要就是这里的用户是指使用设计好的程序的用户,而不是程序设计阶段的。

​ 这种没有服务员的情况其实就是一种用户请求与具体实现之间的强耦合。再举一个甲乙方的例子来看。当甲方下发一个需求的时候,具体的乙方一般是不会看到这个甲方来直接去找到对应的员工来实现对应的功能的。相反,我们通常是把这个命令发送给乙方的对接部门,然后通过一些处理,这些命令会被分包并且下发给对应的实现部门。

​ 我好像有点理解了。所谓的命令模式,要实现的就是一个命令发出者与命令实现之间的一个解耦。这时就需要我们去进行在中间的一层代理,在点餐系统中这是服务员。在项目对接中这个可能是项目经理。但是不变的是,在这种对接中,我们解开了命令发出者和命令执行者之间的强耦合。

​ 通过这种解耦,客户将不用去后厨直接找到对应的厨师进行下单。只需要更具餐馆的规范去进行我们需求的发出,服务员负责添加这些需求到对应的列表中,而服务员接下来还负责将这些个需求按一定顺序发送给对应的后厨部门进行完成,这种分层式的设计使得用户不再需要去了解底层复杂的东西,只需要去了解餐馆提供的简单的菜单即可,其他的逻辑由下面的命令转发者和命令执行者进行操作。

​ 到这里我其实明白了之前的误解。在理解命令模式中,不要在一开始就想得太多。类似与服务员这种消息转发者能够处理一些非常简单的消息转发,就比如用户想要那道菜,服务员就通知后厨做那道菜。除此之外,用户可能还会点套餐,这是一套复杂的命令,可能需要多个处理动作配合使用,这个也是服务员这个中间层需要做的事,但是一开始我们并不需要去拘泥在这里,可以从简单的地方出发。

​ 还有,当我们需要添加菜单时,我们至少也需要去为对应的后厨添加对应的处理动作,这也是我之前存在疑惑的地方,不过现在也解开了一点了。

总的来说,我现在感觉命令模式的核心在于:命令的处理与转发,通过引入一个具有转发行为的类来进行解耦

在最简单的命令模式中,这个转发可以是一个不做任何处理的转发,其他复杂情况再说,现在先从简单的入手。

代码实例

​ 在我看来,理解命令模式中,去看代码甚至比看UML类图更加重要。至少我第一次看的视乎类图根本看不懂它在说什么。

命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Receiver: 后厨
class Kitchen {
public:
void prepareDish(const std::string& dish) {
std::cout << "Kitchen is preparing: " << dish << std::endl;
}
};

// Command: 命令接口
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
};

// ConcreteCommand: 具体命令类
class PrepareDishCommand : public Command {
private:
Kitchen* kitchen;
std::string dish;
public:
PrepareDishCommand(Kitchen* k, const std::string& d) : kitchen(k), dish(d) {}
void execute() override {
kitchen->prepareDish(dish);
}
};

​ 这里的几个类就是命令模式中的基本单位:命令的基本构成。在一个具体的命令中,需要包含这个命令具体的执行部门,所以这里保留了一个后厨的一个指针,用于进行执行部门的指定。我猜想,如果在后期需要对这个后厨部门进行划分,会将这个执行部门进行派生,至于继承还是聚合组合,那不是我们现在考虑的问题。

转发者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Invoker: 服务员
class Waiter {
private:
std::vector<std::shared_ptr<Command>> orders;
public:
void takeOrder(std::shared_ptr<Command> command) {
orders.push_back(command);
}
void sendOrders() {
for (const auto& order : orders) {
order->execute();
}
orders.clear();
}
};

​ 可以看到,这里的转发者维护了一个命令队列,所有的客户给出的命令将会储存在这个队列中。我们还可以观察到这里的命令实际执行情况,其实就是一次程序控制流的转移,将当前的执行权限交给命令类进行自我的执行来实现对应的功能。

​ 这里当然还可以进行一系列的扩展,就比如进行消息的撤销等操作,可以自行添加。

​ 总的来说,这里的转发类重要的是这里的命令处理方法。通过储存客户端发出的所有命令,在适当的时机通过自身设定的命令执行顺序来进行对应的执行。而这里的执行,就跟很多设计模式一样,实际上就是进行一次程序控制流的转移,将程序控制流从当前的中转类转移到对应的命令处理程序中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Client: 客户端
int main() {
// 创建接收者
Kitchen kitchen;

// 创建具体命令
auto steakCommand = std::make_shared<PrepareDishCommand>(&kitchen, "Steak");
auto pastaCommand = std::make_shared<PrepareDishCommand>(&kitchen, "Pasta");

// 创建调用者
Waiter waiter;
waiter.takeOrder(steakCommand);
waiter.takeOrder(pastaCommand);

// 服务员发送订单
waiter.sendOrders();

return 0;
}

​ 这里其实就没有什么好说的,需要注意的是,在命令模式中,我们需要在程序一开始进行所有的命令的初始化,之后我们所有发出的命令本质上其实就是保留了一份对应的指针。当然,你可以在每次调用时都进行副本的一次创建,不给你也应该知道这种模式的坏处。

下面给出一个不是很美的UML类图,我自己都没去看()

UML类图

img

-------------本文结束 感谢阅读-------------