【C++设计模式】命令模式

一、命令模式

所有的设计模式在于不同场景下合理的运用设计模式,接下来介绍一种非常实用的行为型设计模式:命令模式。它使用类封装各个类的行为,将接口统一,这样我们就不用编写复杂的流程控制代码,可以直接基于面向对象的方式开发。

命令模式还有动作模式、事务模式的别名。

这也是行为型设计模式的目标之一:聚焦对象的行为,对象之间如何通信、如何完成自身职责。

简单来说:命令模式就是把操作封装成对象,用来执行动作。可以在面向对象开发中充当回调函数;我们还能把命令对象存入命令队列,尤为适合撤销、重做功能。


在理解本次内容之前,先用一个例子更加深刻了解命令模式。我们经常使用游戏手柄,存在方向控制键、攻击键、跳跃键、拾取键等。这些都是 Command------命令,也就是基类。如果能把所有的动作封装为同一个基类的派生类,所有具体命令继承于基类 Command,例如向左移动只需要操作基类对象,X 跳跃键也只需要操作 Command 基类即可,不需要关心具体哪一条指令。举代码示意:X 可以赋值为某命令对象,比如跳跃指令。这套方案的强大之处:行为全部封装成对象,可以灵活控制。

例如可以调用 x.Execute(),Execute 就是命令基类的成员函数,所有命令都必须实现 execute 方法。这里用到 virtual void 函数 = 0 代表纯虚函数,派生类强制实现,也就是:JumpCommand(跳跃命令)必须重写 Execute,其他命令也如此。


这套模式的另一个优势:命令对象可以延迟执行,管控执行时机。按下 X 时,可以立即执行跳跃,也可以先缓存指令延后执行。可以用容器存放多条命令:命令 1、命令 2、命令 3......,存储容器可以选用队列、栈。C++ 中 vector 常用存储命令指针(需要管理生命周期,防止内存泄露),遍历容器,依次调用每条命令的 execute。

除了 execute,还可以添加 undo 撤销功能,用来回滚操作。我们遍历存放命令的容器,不仅可以批量执行,也可以批量撤销。撤销逻辑由开发者定义:跳跃的撤销可以是回到起跳前坐标;文字编辑器删除文本后,撤销就是恢复被删除的文字。

整套命令执行记录构成操作历史,放在容器中。

二、代码编写

  • 我们先要统一抽象接口,定义一个Command类,当按下游戏手柄的跳跃键,可以立即执行该动作,也可以按下撤销,回退上一次动作。因此我们要抽象出两个接口,接口都是在操作对象的行为,因此还要传入对象的引用来指定哪一个对象执行什么动作
cpp 复制代码
class Command
{
public:
    Command() {}
    virtual ~Command() {}
    virtual void Execute(Character&) = 0; // 定义纯虚函数,所有派生类都要实现
    virtual void Undo(Character&) = 0;    // 撤销
};
  • 对象类的设计使用结构体,默认是public。此外还要有角色名称,位置坐标等属性。定义角色本身具有移动属性,可以执行对应的动作。Move是指令-由外部下达,move是具体动作的执行。并且对于经常调用的行为放在Character里较好
cpp 复制代码
struct Character
{
    Character(std::string _name):name(_name), x(0), y(0){}
    void move(int _x, int _y) { x = _x; y = _y; }

    int x;
    int y;
    std::string name;
};
  • 派生类Move指令公有继承基类Command,由基类定义可得派生类必须实现Execute函数和Undo撤销函数。并且在Move之前还需要保存原始坐标位置,用来Undo,这两个功能的实现都需要调用character对象内部的move函数
cpp 复制代码
class Move : public Command
{
public:
    void Execute(Character& c)
    {
        oldx = c.x, oldy = c.y;
        int newx = rand() % 4, newy = rand() % 4;
        c.move(newx, newy);
        printf("%s moved, x = %d, y = %d\n", c.name.c_str(), c.x, c.y);
    }
    void Undo(Character& c)
    {
        c.move(oldx, oldy);
        printf("%s undo , x = %d, y = %d\n", c.name.c_str(), c.x, c.y);
    }
private:
    int oldx;
    int oldy;
};
  • 主函数需要定义命令队列,用来存储历史动作的智能指针,用于后续的撤销。使用智能指针防止内存泄露。vector中传入的派生类Move匿名对象的智能指针
  • 后续execute时,因为传入的是Move的指针,所以直接调用execute方法是对传入的对象执行Move
  • 如果还有Jump类,可以增加三维坐标。只是后续传入vector中的是Jump的指针,也通过直接调用execute方法执行jump动作,所以命令模式具有统一接口的功能
cpp 复制代码
int main()
{
    srand(time(nullptr));
    Character lio("lio");

    vector<unique_ptr<Command>> command_queue;
    
    command_queue.push_back(make_unique<Move>());
    command_queue.push_back(make_unique<Move>());
    command_queue.push_back(make_unique<Move>());
    command_queue.push_back(make_unique<Move>());

    for(auto &m : command_queue)
    {
        m->Execute(lio);
    }
    for(int i=command_queue.size()-1; i>=0 ; i--)
    {
        command_queue[i]->Undo(lio);
    }
    return 0;
}
  • 完整代码
cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
#include <memory>

using namespace std;

struct Character
{
    Character(std::string _name):name(_name), x(0), y(0){}
    void move(int _x, int _y) { x = _x; y = _y; }

    int x;
    int y;
    std::string name;
};

class Command
{
public:
    Command() {}
    virtual ~Command() {}
    virtual void Execute(Character&) = 0; // 定义纯虚函数,所有派生类都要实现
    virtual void Undo(Character&) = 0;    // 撤销
};

class Move : public Command
{
public:
    void Execute(Character& c)
    {
        oldx = c.x, oldy = c.y;
        int newx = rand() % 4, newy = rand() % 4;
        c.move(newx, newy);
        printf("%s moved, x = %d, y = %d\n", c.name.c_str(), c.x, c.y);
    }
    void Undo(Character& c)
    {
        c.move(oldx, oldy);
        printf("%s undo , x = %d, y = %d\n", c.name.c_str(), c.x, c.y);
    }
private:
    int oldx;
    int oldy;
};


int main()
{
    srand(time(nullptr));
    Character lio("lio");

    vector<unique_ptr<Command>> command_queue;
    
    command_queue.push_back(make_unique<Move>());
    command_queue.push_back(make_unique<Move>());
    command_queue.push_back(make_unique<Move>());
    command_queue.push_back(make_unique<Move>());

    for(auto &m : command_queue)
    {
        m->Execute(lio);
    }
    for(int i=command_queue.size()-1; i>=0 ; i--)
    {
        command_queue[i]->Undo(lio);
    }
    return 0;
}

三、可能出现的问题

  • 如果在栈上创建Move对象,存入vector会报错,应该用new创建命令对象存入容器,手动new后需要delete释放内存

基类抽象接口参数和派生类接口参数必须一致

  • 抽象类不允许实例化
  • unique_ptr只能移动不能复制

解决:

相关推荐
wuminyu1 小时前
JDK21 FFM异步读取MSG_ZEROCOPY错误队列揭秘
java·linux·c语言·jvm·c++
用户6919026813391 小时前
Agent 上下文记忆原理
设计模式·架构·设计
江安下小雨1 小时前
muduo网络库(十三):Buffer 缓冲区类
c++·经验分享
Java后端的Ai之路2 小时前
02、Python普通工厂模式
开发语言·人工智能·python·设计模式·普通工厂模式
liulilittle3 小时前
XTCP简述
网络·c++·tcp/ip·ip·tcp·通信
一只旭宝3 小时前
预约系统版本2(基于第一版改良)
服务器·数据库·c++
被怪兽吃掉了3 小时前
5.2.1一维组数定义方式
开发语言·c++·算法
旖旎夜光3 小时前
LeetCode 69:x 的平方根(二分查找) —— 题解
数据结构·c++·算法·leetcode·二分查找
j7~3 小时前
【C++】《C++二叉搜索树(BST)从入门到精通:概念、实现与Key/Value模型全解析》
开发语言·c++·学习·二叉搜索树