【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只能移动不能复制

解决:

相关推荐
左手の明天1 天前
SLIP 协议封装与解析详解:原理、C 代码与实战案例
linux·c语言·开发语言·c++
青少儿编程课堂1 天前
后缀自动机解析:本质不同子串与最长重复片段统计
c++·python·算法·bfs·信息学竞赛
朝朝辞暮i1 天前
C++ 第 12 课:局部变量、作用域、变量生命周期
开发语言·c++·算法
无名猿1 天前
shared_ptr 完全指南:引用计数、控制块与开销
c++·性能优化·内存管理·标准库·现代c++
geovindu1 天前
rust: Composite Pattern
开发语言·后端·设计模式·rust·组合模式
xailorliu1 天前
SLIC_超像素算法
c++·opencv·目标检测·机器学习·计算机视觉
苏打豆1 天前
CRISP源码阅读——基于ROS2力矩反馈控制器与速度加速度零空间
c++·线性代数·算法·矩阵·机器人
小小龙学IT1 天前
snap7 开源西门子 S7 通信协议库深度解析
c++·开源
小溪学编程1 天前
从 C++ 的规范变迁看语言发展
jvm·c++·面试