C++ 设计模式——命令模式
命令(Command)模式是一种行为型模式,它将请求封装为对象,从而使您能够使用不同的请求、排队请求或记录请求,以及支持可撤销的操作。
主要组成部分
- 命令接口(Command):定义了一个执行操作的接口,通常包含一个核心方法,该方法负责执行请求。
- 具体命令(ConcreteCommand):实现了命令接口,并定义了与接收者之间的绑定关系。在执行请求时,它会调用接收者的方法。
- 接收者(Receiver):知道如何实施与执行相关的操作。它是执行请求的实际对象。
- 调用者(Invoker):持有命令对象,并在适当的时候调用它们。它通常不直接执行操作,而是通过命令对象来执行。
- 客户端(Client):创建具体命令对象,并将其与接收者关联。客户端负责创建和管理命令对象。
构建过程
命令模式的构建过程,以点菜为例。在这个场景中,命令模式使得服务员(调用者)和厨师(接收者)之间的交互更加灵活和清晰。服务员不需要知道厨师如何制作菜品,只需传递订单便签即可。而厨师只需按照订单便签制作相应的菜品。这样,整个点餐流程更加模块化和易于管理。
-
命令接口(Command):
-
代码定义了一个名为
Command
的抽象类,它代表了一个通用的命令接口。 -
在点菜场景中,这个接口可以被看作是服务员用来传递给厨师的一个“订单便签”,它告诉厨师需要做什么菜。
class Command { public: Command(Cook* pcook) : m_pcook(pcook) {} virtual ~Command() { delete m_pcook; } virtual void Execute() = 0; protected: Cook* m_pcook; // 接收者 };
-
-
具体命令(ConcreteCommand):
CommandFish
和CommandMeat
类是Command
接口的具体实现,它们代表具体的订单,比如“做红烧鱼”和“做锅包肉”。
class CommandFish : public Command { public: CommandFish(Cook* pcook) : Command(pcook) {} void Execute() override { m_pcook->cook_fish(); // 调用接收者的方法 } }; class CommandMeat : public Command { public: CommandMeat(Cook* pcook) : Command(pcook) {} void Execute() override { m_pcook->cook_meat(); // 调用接收者的方法 } };
-
接收者(Receiver):
Cook
类代表接收者,即厨师,他知道如何制作菜品。- 在点菜场景中,厨师根据服务员传递的订单便签(命令对象)来制作菜品。
class Cook { public: void cook_fish() { cout << "做一盘红烧鱼菜品" << endl; } void cook_meat() { cout << "做一盘锅包肉菜品" << endl; } };
-
调用者(Invoker):
Waiter
类代表调用者,即服务员,他负责接收顾客的订单并传递给厨师。- 服务员保存了一个命令列表,并负责在适当的时机调用这些命令,通知厨师开始制作菜品。
class Waiter { public: void AddCommand(Command* pcommand) { m_commlist.push_back(pcommand); } void Notify() { for (auto iter = m_commlist.begin(); iter != m_commlist.end(); ++iter) { (*iter)->Execute(); // 调用命令的执行方法 } } private: std::list<Command*> m_commlist; // 命令列表 };
-
客户端(Client):
main
函数作为客户端,模拟顾客下订单的过程。- 客户端创建服务员和厨师对象,以及具体的命令对象,并将命令对象添加到服务员的订单列表中。
int main() { // 1. 客户端创建接收者 厨师 Cook* cook1 = new Cook(); Cook* cook2 = new Cook(); // 2. 客户端创建具体命令对象并设置接收者 订单列表 Command* pcmd1 = new CommandFish(cook1); Command* pcmd2 = new CommandMeat(cook2); // 3. 客户端将命令对象传递给调用者 服务员 Waiter* pwaiter = new Waiter(); pwaiter->AddCommand(pcmd1); pwaiter->AddCommand(pcmd2); // 4. 调用者请求执行操作 服务员通知厨师 pwaiter->Notify(); // 释放资源 delete pwaiter; delete pcmd2; delete pcmd1; return 0; }
命令模式 UML 图
UML 图解析
- Receiver(接收者类):知道如何实施与执行请求相关的操作,这里指
Cook
类。提供了对请求的业务处理接口(cook_fish
、cook_meat
)。 - Invoker(调用者类):请求的发送者,通过命令对象来执行请求,这里指
Waiter
类。该类只与抽象命令类Command
之间存在关联关系。 - Command(抽象命令类):声明执行操作的接口,这里指的是
Command
类。在其中声明了用于执行请求的Execute
方法。 - ConcreteCommand(具体命令类):抽象命令类的子类,这里指的是
CommandFish
类和CommandMeat
类。类中实现了执行请求的Execute
方法来调用接收者类Cook
中的相关操作。 - Client(客户端):创建具体的命令类对象并设定它的接收者。这里指main主函数中的几行代码。
命令模式的优点
- 解耦:命令模式将请求的发送者和接收者解耦,降低了它们之间的耦合度。
- 可扩展性:可以轻松添加新的命令而不修改现有代码。
- 支持撤销操作:可以实现命令的撤销和重做功能。
- 支持排队请求:可以将请求排队并在需要时执行。
命令模式的缺点
- 命令类的数量可能会增加:如果有很多操作,可能会导致命令类的数量激增。
- 复杂性增加:引入命令模式可能会增加系统的复杂性。
命令模式适用场景
- 请求解耦:当需要将请求的发送者(调用者)与请求的接收者(接收者)解耦时。比如,在图形用户界面中,按钮点击事件可以通过命令模式实现,按钮不需要直接调用处理逻辑,而是通过命令对象进行。
- 支持撤销和重做操作:当需要实现撤销和重做功能时。命令对象可以被存储在历史记录中,用户可以随时撤销或重做操作。例如,在文本编辑器中,用户可以撤销或重做输入的文本操作。
- 请求排队或记录请求:当需要将请求排队或记录请求时。命令模式允许将多个请求存储在一个队列中,稍后统一处理。这在任务调度或异步处理场景中非常有用。
- 实现宏命令:当需要实现宏命令(多个命令的组合)时。可以将多个命令组合成一个命令对象,方便一次性执行多个操作。例如,在游戏中,可以将多个动作(如移动、攻击、跳跃)组合成一个宏命令。
- 动态命令:当需要在运行时动态创建和组合命令时。命令模式允许在运行时根据用户输入或其他条件生成新的命令对象,增强系统的灵活性。
- 日志记录:当需要记录操作日志时。通过命令对象,可以记录每个操作的详细信息,方便后续审计和分析。
- 多线程环境:在多线程环境中,命令模式可以帮助管理任务的执行,确保线程安全地执行命令。
总结
命令模式通过将请求封装为对象,使得请求的发送者和接收者之间的耦合度降低,增强了系统的灵活性和可扩展性。它不仅适用于简单的命令执行场景,还可以扩展到复杂的请求管理和操作历史记录等场景。
完整代码
#include <iostream>
#include <list>
using namespace std;
//厨师类
class Cook
{
public:
//做红烧鱼
void cook_fish()
{
cout << "做一盘红烧鱼菜品" << endl;
}
//做锅包肉
void cook_meat()
{
cout << "做一盘锅包肉菜品" << endl;
}
//做其他各种菜品......略
};
//-------------------------
//厨师做的每样菜品对应的抽象类
class Command
{
public:
Command(Cook* pcook)
{
m_pcook = pcook;
}
//做父类时析构函数应该为虚函数
virtual ~Command()
{
if (m_pcook != nullptr)
{
delete m_pcook;
m_pcook = nullptr;
}
}
virtual void Execute() = 0;
protected:
Cook* m_pcook; //子类需要访问
};
//做红烧鱼菜品命令(顾客下的红烧鱼菜品便签)
class CommandFish :public Command
{
public:
CommandFish(Cook* pcook) :Command(pcook) {}
virtual void Execute()
{
m_pcook->cook_fish();
}
};
//做锅包肉菜品命令(顾客下的锅包肉菜品便签)
class CommandMeat :public Command
{
public:
CommandMeat(Cook* pcook) :Command(pcook) {}
virtual void Execute()
{
m_pcook->cook_meat();
}
};
//-----------------------------
//服务员类
class Waiter
{
public:
//将顾客的便签增加到便签列表中,即便一个便签中包含多道菜品,这也相当于一道一道菜品加入到列表中
void AddCommand(Command* pcommand)
{
m_commlist.push_back(pcommand);
}
void DelCommand(Command* pcommand) //如果顾客想撤单则将便签从列表中删除
{
m_commlist.remove(pcommand);
}
void Notify() //服务员将所有便签一次性交到厨师手里让厨师开始按顺序做菜
{
//依次让厨师做每一道菜品
for (auto iter = m_commlist.begin(); iter != m_commlist.end(); ++iter)
{
(*iter)->Execute();
}
}
private:
//一个便签中可以包含多个菜品甚至可以一次收集多个顾客的便签,达到一次性通知厨师做多道菜的效果
std::list<Command*> m_commlist; //菜品列表,每道菜品作为一项,如果一个便签中有多个菜品,则这里将包含多项
};
int main()
{
//一次性在便签上写下多道菜品
Command* pcmd1 = new CommandFish(new Cook());
Command* pcmd2 = new CommandMeat(new Cook());
Waiter* pwaiter = new Waiter();
//把多道菜品分别加入到菜品列表中
pwaiter->AddCommand(pcmd1);
pwaiter->AddCommand(pcmd2);
//服务员一次性通知厨师做多道菜
pwaiter->Notify();
// 释放资源
delete pwaiter;
delete pcmd2;
delete pcmd1;
return 0;
}