行为型设计模式——命令模式

文章目录

命令模式

在软件开发中,我们经常需要向某些对象发送请求(调用其中的某个或某些方法),但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,此时,我们特别希望能够以一种松耦合的方式来设计软件,使得请求发送者与请求接收者能够消除彼此之间的耦合,让对象之间的调用关系更加灵活,可以灵活地指定请求接收者以及被请求的操作。命令模式(Command Pattern)为此类问题提供了一个较为完美的解决方案。

命令模式就是将请求转换为一个包含与请求相关的所有信息的独立对象,通过这个转换能够让使用者根据不同的请求将客户参数化、 延迟请求执行或将请求放入队列中或记录请求日志, 且能实现可撤销操作。

命令模式的别名为动作(Action)模式或事务(Transaction)模式。
场景

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

尽管所有按钮看上去都很相似, 但它们可以完成不同的操作 (打开、 保存、 打印和应用等)。你可能使用继承去处理具体的按钮操作逻辑。

这种方式有严重缺陷。 首先, 你创建了大量的子类, 当每次修改基类 按钮时, 你都有可能需要修改所有子类的代码。再次,可能会存在多个类实现同一个功能的现象(如:保存按钮、保存菜单项按钮,保存快捷键等等),需要复制大量的代码。怎么解决呢?

那就是将GUI 和业务逻辑功能剥离开来,通过传递数据参数完成交互

命令模式建议GUI 对象不直接提交这些请求。 你应该将请求的所有细节 (例如调用的对象、 方法名称和参数列表) 抽取出来组成命令类, 该类中仅包含一个用于触发请求的方法。

命令对象负责连接不同的 GUI 和业务逻辑对象。 此后, GUI 对象无需了解业务逻辑对象是否获得了请求, 也无需了解其对请求进行处理的方式。 GUI 对象触发命令即可, 命令对象会自行处理所有细节工作。如下面的设计方式。

应用命令模式后,我们不再需要任何按钮子类来实现点击行为。 我们只需在 按钮Button基类中添加一个成员变量来存储对于命令对象的引用, 并在点击后执行该命令即可。

结构

在命令模式结构图中包含如下几个角色:

  • Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
  • ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
  • Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
  • Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。

实现

cpp 复制代码
//这个厨师类是命令模式中命令的接收者,收不到命令厨师是不能工作的
// 厨师哲普
class CookerZeff
{
public:
    void makeDSX()
    {
        cout << "开始烹饪地三鲜...";
    }
    void makeGBJD()
    {
        cout << "开始烹饪宫保鸡丁...";
    }
    void makeYXRS()
    {
        cout << "开始烹饪鱼香肉丝...";
    }
    void makeHSPG()
    {
        cout << "开始烹饪红烧排骨...";
    }
};
cpp 复制代码
// 点餐的命令 - 抽象类
class AbstractCommand
{
public:
    AbstractCommand(CookerZeff* receiver) : m_cooker(receiver) {}
    virtual void excute() = 0;
    virtual string name() = 0;
    ~AbstractCommand() {}
protected:
    CookerZeff* m_cooker = nullptr;
};
cpp 复制代码
//顾客下单就是命令模式中的命令,这些命令的接收者是厨师,命令被分离出来实现了和厨师类的解耦合。
//通过这种方式可以控制命令执行的时机,毕竟厨师都是在顾客点餐完毕之后才开始炒菜的。

// 地三鲜的命令
class DSXCommand : public AbstractCommand
{
public:
    using AbstractCommand::AbstractCommand;
    void excute() override
    {
        m_cooker->makeDSX();
    }
    string name() override
    {
        return "地三鲜";
    }
};

// 宫保鸡丁的命令
class GBJDCommand : public AbstractCommand
{
public:
    using AbstractCommand::AbstractCommand;
    void excute() override
    {
        m_cooker->makeGBJD();
    }
    string name() override
    {
        return "宫保鸡丁";
    }
};

// 鱼香肉丝的命令
class YXRSCommand : public AbstractCommand
{
public:
    using AbstractCommand::AbstractCommand;
    void excute() override
    {
        m_cooker->makeYXRS();
    }
    string name() override
    {
        return "鱼香肉丝";
    }
};

// 红烧排骨的命令
class HSPGCommand : public AbstractCommand
{
public:
    using AbstractCommand::AbstractCommand;
    void excute() override
    {
        m_cooker->makeHSPG();
    }
    string name() override
    {
        return "红烧排骨";
    }
};
cpp 复制代码
// 服务器路飞 - 命令的调用者
class WaiterLuffy
{
public:
    // 下单
    void setOrder(int index, AbstractCommand* cmd)
    {
        cout << index << "号桌点了" << cmd->name() << endl;
        if (cmd->name() == "鱼香肉丝")
        {
            cout << "没有鱼了, 做不了鱼香肉丝, 点个别的菜吧..." << endl;
            return;
        }
        // 没找到该顾客
        if (m_cmdList.find(index) == m_cmdList.end())
        {
            list<AbstractCommand*> mylist{ cmd };
            m_cmdList.insert(make_pair(index, mylist));
        }
        else
        {
            m_cmdList[index].push_back(cmd);
        }
    }
    // 取消订单
    void cancelOrder(int index, AbstractCommand* cmd)
    {
        if (m_cmdList.find(index) != m_cmdList.end())
        {
            m_cmdList[index].remove(cmd);
            cout << index << "号桌, 撤销了" << cmd->name() << endl;
        }
    }
    // 结账
    void checkOut(int index)
    {
        cout << "第[" << index << "]号桌的顾客点的菜是: 【";
        for (const auto& item : m_cmdList[index])
        {
            cout << item->name();
            if (item != m_cmdList[index].back())
            {
                cout << ", ";
            }
        }
        cout << "】" << endl;
    }
    void notify(int index)
    {
        for (const auto& item : m_cmdList[index])
        {
            item->excute();
            cout << index << "号桌" << endl;
        }
    }
private:
    // 存储顾客的下单信息
    /*key 值是顾客就餐的餐桌编号, value 值存储的是顾客所有的点餐信息。
    *并且这个 value 是一个 list 容器,用于存储某个顾客的所有的点餐信息。
    */
    map<int, list<AbstractCommand*>> m_cmdList;
};
cpp 复制代码
int main()
{
    CookerZeff* cooker = new CookerZeff;
    WaiterLuffy* luffy = new WaiterLuffy;

    YXRSCommand* yxrs = new YXRSCommand(cooker);
    GBJDCommand* gbjd = new GBJDCommand(cooker);
    DSXCommand* dsx = new DSXCommand(cooker);
    HSPGCommand* hspg = new HSPGCommand(cooker);

    cout << "=================== 开始点餐 ===================" << endl;
    luffy->setOrder(1, yxrs);
    luffy->setOrder(1, dsx);
    luffy->setOrder(1, gbjd);
    luffy->setOrder(1, hspg);
    luffy->setOrder(2, dsx);
    luffy->setOrder(2, gbjd);
    luffy->setOrder(2, hspg);
    cout << "=================== 撤销订单 ===================" << endl;
    luffy->cancelOrder(1, dsx);
    cout << "=================== 开始烹饪 ===================" << endl;
    luffy->notify(1);
    luffy->notify(2);
    cout << "=================== 结账 ===================" << endl;
    luffy->checkOut(1);
    luffy->checkOut(2);

    return 0;
}

命令队列

有时候我们需要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,我们可以通过命令队列来实现。

命令队列的实现方法有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类来负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者。

cpp 复制代码
// 专门负责维护和操作命令的集合
class CommandQueue
{
public:
    // 添加命令到队列
    void addCommand(int index, AbstractCommand* cmd)
    {
        m_cmdList[index].push_back(cmd);
    }

    // 从队列中移除命令
    void removeCommand(int index, AbstractCommand* cmd)
    {
        if (m_cmdList.find(index) != m_cmdList.end())
        {
            m_cmdList[index].remove(cmd);
        }
    }

    // 执行某个桌号的所有命令
    void executeQueue(int index)
    {
        if (m_cmdList.find(index) == m_cmdList.end()) return;
        
        for (const auto& item : m_cmdList[index])
        {
            item->execute(); // 厨师开始做菜
            cout << index << "号桌" << endl;
        }
    }

    // 打印某个桌号的账单明细
    void printQueue(int index)
    {
        if (m_cmdList.find(index) == m_cmdList.end() || m_cmdList[index].empty()) {
            cout << "【无】" << endl;
            return;
        }

        cout << "【";
        for (const auto& item : m_cmdList[index])
        {
            cout << item->name();
            if (item != m_cmdList[index].back())
            {
                cout << ", ";
            }
        }
        cout << "】" << endl;
    }

private:
    // 将原属于服务员的存储结构移交到队列类中
    map<int, list<AbstractCommand*>> m_cmdList;
};
cpp 复制代码
class WaiterLuffy
{
public
:
    // 下单
    void setOrder(int index, AbstractCommand* cmd)
    {
        cout << index << "号桌点了" << cmd->name() << endl
;
        if (cmd->name() == "鱼香肉丝"
)
        {
            cout << "没有鱼了, 做不了鱼香肉丝, 点个别的菜吧..." << endl
;
            return
;
        }
        // 将具体的存储和逻辑外包给命令队列
        m_queue.addCommand(index, cmd);
    }

    // 取消订单
    void cancelOrder(int index, AbstractCommand* cmd)
    {
        m_queue.removeCommand(index, cmd);
        cout << index << "号桌, 撤销了" << cmd->name() << endl
;
    }

    // 结账
    void checkOut(int index)
    {
        cout << "第[" << index << "]号桌的顾客点的菜是: "
;
        m_queue.printQueue(index);
    }

    // 通知执行
    void notify(int index)
    {
        m_queue.executeQueue(index);
    }

private
:
    CommandQueue m_queue; 
// 组合了命令队列类
};
  • CommandQueue(命令队列类) :专门负责管理某一桌或者整个餐厅的命令队列。这里我们让它管理 map<int, list<AbstractCommand*>>
  • WaiterLuffy(服务员类) :内部持有一个 CommandQueue 的引用或指针。服务员接收到顾客的需求后,调用队列的方法来加菜、退菜,最后通知队列执行。

特点

主要优点

  • 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
  • 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足"开闭原则"的要求。
  • 可以比较容易地设计一个命令队列
  • 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。

主要缺点

  • 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。

适用环境

  • 如果你需要通过操作来参数化对象,可使用命令模式。
    • 命令模式可将特定的方法调用转化为独立对象。这一改变也带来了许多有趣的应用: 你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。
  • 如果你想要将操作放入队列中、操作的执行或者远程执行操作,可使用命令模式。
    • 同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。
  • 如果你想要实现操作回滚功能,可使用命令模式。
    • 为了能够回滚操作, 你需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。
相关推荐
我能坚持多久1 小时前
STL详解——list的介绍以及功能展示
开发语言·c++
大大杰哥1 小时前
2026陕西省ICPC省赛补题(前六题)
c++·算法
Brilliantwxx1 小时前
【C++】 继承与多态(上)
开发语言·c++·笔记·算法
不负岁月无痕2 小时前
STL -- C++ string 类 模拟实现
java·开发语言·c++
·心猿意码·2 小时前
OCCT源码解析(六):TKG3d 模块——三维曲面体系
c++·3d
会开花的二叉树2 小时前
Qt初体验-第一个窗口程序踩的坑
开发语言·c++·qt
思麟呀2 小时前
在C++基础上理解CSharp-3
开发语言·c++·c#
Geometry Fu2 小时前
《设计模式》2026编程作业汇总
java·c++·设计模式
十五年专注C++开发2 小时前
QtFluentWidgets: 一套基于C++ Qt Widgets的Fluent Design风格控件库
开发语言·c++·qt·qtfluentwidgets