一、命令模式
所有的设计模式在于不同场景下合理的运用设计模式,接下来介绍一种非常实用的行为型设计模式:命令模式。它使用类封装各个类的行为,将接口统一,这样我们就不用编写复杂的流程控制代码,可以直接基于面向对象的方式开发。
命令模式还有动作模式、事务模式的别名。
这也是行为型设计模式的目标之一:聚焦对象的行为,对象之间如何通信、如何完成自身职责。
简单来说:命令模式就是把操作封装成对象,用来执行动作。可以在面向对象开发中充当回调函数;我们还能把命令对象存入命令队列,尤为适合撤销、重做功能。
在理解本次内容之前,先用一个例子更加深刻了解命令模式。我们经常使用游戏手柄,存在方向控制键、攻击键、跳跃键、拾取键等。这些都是 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只能移动不能复制

解决:

