1 概述
命令设计模式是一种常见的设计模式,通过对请求指令进行封装,达到指令请求者与指令接收者解耦的目标,从而实现行为指令请求的有序化管理。该设计模式常用于各类软件的撤销与恢复等组合功能。
2 命令设计模式
命令设计模式是一个高内聚的模式,其标准定义:Encapsulate a request as an object,thereby letting you parameterize clients with different requests,queue or log requests ,and support undoable operations。(请将一个请求封装为一个对象,从而让你使不同的请求把客户端参数化,对请求排队或者记录请求日,支持请求的撤销与恢复功能)。
上面的概念是比较抽象的,下面以餐厅顾客点菜为例说明命令设计模式。首先,顾客1招呼服务员自己需要点一份西红柿炒蛋,服务员记录顾客1的点菜要求,同时顾客2需要点一份小炒肉,服务员再次记录顾客2的点菜要求,然后服务员将顾客的点菜反馈至厨师,厨师开始做菜。案例中顾客就是请求的发起者,服务员对多个顾客的点菜请求进行统一记录,并将点菜请求发送给厨师,这样顾客与厨师没有直接交互,下面绘制出上面过程的UML图。
指令模式通常包含三大角色,指令接收者Receiver、指令对象Command、指令调用者Invoker三类。指令接收者Receiver通常为执行指令具体内容的对象,例如厨师接受做菜指令;指令对象Command包含所有需操作的指令声明;Invoker为指令调用者,依据具体场景进行指令的实例化与传递。指令模式的通用类图如下。
通过上图,Invoker是指令的发起者,Reciver是指令的执行者,两者无直接交互。调用者只需要调用Command的抽象方法Execute即可实现指令内容的执行,实现指令调用者与指令接收者的解耦。
3 设计案例说明
以一个控制显示界面中控件的新建、删除、复制与粘贴过程为例,结合指令设计模式说明撤销与重做功能的实现。
3.1 原始功能
在不考虑指令模式的情况下,控制显示窗口支持文本控件、灯控件与按钮控件的显示与编辑,每类控件均支持创建、复制、删除与粘贴操作,具体如下图所示。
根据软件实现逻辑给出控制显示面板与各类控件的UML类图。其中,CControlView为控制显示面板,包含控件创建、控件删除与控件粘贴等成员函数函数;显示控件基类为CShowItem,其关键成员函数为初始化函数、编辑控件与克隆控件虚函数,三类子控件集成基类显示控件,并实现基类需函数。
3.2 改进功能
在上述功能基础上,提出支持控制显示面板上各类控件的创建、复制、粘贴与删除操作的撤销与重做需求,基于该需求对上述代码进行改造。改造主要分为三部分内容,确定控制指令类的核心功能、指令控制类的核心功能以及使用指令替换原始各类控件操作,下面进行介绍说明。
3.2.1 控制指令封装控件操作
基于命令模式与上述控制显示需求,将控件的创建、复制、粘贴与删除操作封装为指令类,指令包含最基本的操作Execute()、Undo()、Redo()三个成员函数,指令类内部需要明确指令接收者Reciver,其中指令类型采用枚举变量CmdType 枚举实现。
3.2.2 控制指令管理类存储指令
创建一个指令管理类,用于存放执行的指令,便于实现指令的撤销与重做。指令管理类内部包含一个存放执行过指令的队列与一个指向队列特定元素的指令指针;当执行新指令时,向指令队列添加新指令,并调用执行指令(调用指令Execute()函数),并且将指令指针指向队列最新元素;当撤销指令时,则依据指令指针从队列中取出一个指令,执行指令撤销操作(调用指令Undo()函数),并将指令指针指向队列的前一个元素;当重做指令时,则依据指令指针取出指令,执行指令重做操作(调用指令Redodo()函数,并将指令指针指向队列的下一个元素。上述操作过程中,指令队列及指令指针变化情况如下图示意。
3.2.3 控制指令替换操作函数
使用新增的指令与指令管理类响应各类控件的UI操作,具体在控件创建、复制、粘贴与删除操作事件中,通过调用CCommandManager与CControlCommand实现各类操作。
4 应用代码实现
结合第3节改进功能,对控制指令与控制指令管理类的代码进行简要介绍。
4.1 控制指令类
控制指令类通过CMDTYE枚举实现指令类别标识,并且不同指令在初始化时使用不同构造函数,具体说明如下:
- 创建控件指令构造函数,传参为父控件、控件位置、控件类型、指令类型默认为创建类型;
- 复制与删除控制指令构造函数,传参为父控件、控件指针、指令类型;
- 粘贴控件指令构造函数,传参为父控件、控件位置、复制控件指针、指令类型。
指令类头文件ControlCommand.h具体函数形式如下代码所示:
class CShowItem; /* 向前申明 */
class CControlView; /* 向前申明 */
class CControlCommand
{
public:
enum RUNSTATUS {
Running = 0,
NotRun = 1
};
enum CMDTYPE {
NONE = 0, /* 默认 */
CREATE = 1, /* 创建 */
COPY = 2, /* 复制 */
PASTE = 3, /* 粘贴 */
REMOVE = 4 /* 移除 */
};
private:
/* 视图容器*/
CControlView* _pViewContianer;
/* 命令名称 */
QString _cmdName;
/* 命令状态 */
RUNSTATUS _eStatus;
/* 命令类型 */
CMDTYPE _eCmdType;
/* 控件类型 */
QString _ItemType;
/* 控件位置 */
QPoint _Position;
/* 控件对象*/
CShowItem* _ShowItem;
/* 新控件对象*/
CShowItem* _NewShowItem;
/* 指令执行结果 */
bool cmdResStatus;
public:
CControlCommand(CControlView* parentView);
CControlCommand(CControlView* parentView, QPoint pos, QString itemType, CMDTYPE cmdtype); /* 创建*/
CControlCommand(CControlView* parentView, CShowItem* item, CMDTYPE cmdtype); /* 复制 、删除*/
CControlCommand(CControlView* parentView, QPoint pos, CShowItem* item, CMDTYPE cmdtype); /* 粘贴 */
~CControlCommand(void);
/* 获取指令名称 */
QString GetCmdName() { return _cmdName; };
/* 执行指令 */
bool Execute();
/* 是否可执行 */
bool IsEnabled();
/* 重做 */
void Redo();
/* 撤销 */
void Undo();
/* 设置运行状态 */
void SetCmdStatus(RUNSTATUS inputStatus) { _eStatus = inputStatus; }
/* 获取运行状态 */
RUNSTATUS GetStatus() { return _eStatus; };
};
#endif // _C_CONTROL_COMAND_
指令类实现文件ControlCommand.cpp文件对头文件中声明的函数进行实现,其中重点为Execute()、Undo()、Redo()函数,其中Undo()函数与Execute()函数对应指令操作互逆,Redo()函数与Execute()操作一致,下面进行简要说明:
- 指令类型为创建控件指令时,Execute()函数实现对应创建,Undo函数则实现删除操作;
- 指令类型为复制控件指令时,Execute()函数实现复制控件记录,Undo函数则清除复制控件记录信息;
- 指令类型为粘贴控件指令时,Execute()函数实现对应控件粘贴,Undo函数则实现粘贴控件删除操作;
- 指令类型为删除控件指令时,Execute()函数实现对应控件删除,Undo函数则实现删除控件创建操作。
/* 构造函数 */
CControlCommand::CControlCommand(CControlView* parentView)
{
_pViewContianer = parentView;
_cmdName = "";
_eStatus = RUNSTATUS::NotRun;
_eCmdType = CMDTYPE::NONE;
_ShowItem = nullptr;
_NewShowItem = nullptr;
};
/* 创建控件指令构造函数 */
CControlCommand::CControlCommand(CControlView* parentView, QPoint pos, QString itemType, CMDTYPE cmdtype)
{
_pViewContianer = parentView;
_eCmdType = cmdtype;
_Position = pos;
_ItemType = itemType;
_cmdName = "";
if (_eCmdType == CMDTYPE::CREATE)
{
_cmdName = "创建控件";
}
_eStatus = RUNSTATUS::NotRun;
_ShowItem = nullptr;
_NewShowItem = nullptr;
cmdResStatus = false;
}
/* 复制、删除 */
CControlCommand::CControlCommand(CControlView* parentView, CShowItem* item, CMDTYPE cmdtype)
{
_pViewContianer = parentView;
_ShowItem = item;
_eCmdType = cmdtype;
_cmdName = "";
if (_eCmdType == CMDTYPE::COPY)
{
_cmdName = "复制控件";
}
else if (_eCmdType == CMDTYPE::REMOVE)
{
_cmdName = "移除控件";
}
if (nullptr != _ShowItem)
{
_Position = _ShowItem->GetPosition();
_ItemType = _ShowItem->ItemType();
}
_eStatus = RUNSTATUS::NotRun;
_NewShowItem = nullptr;
cmdResStatus = false;
}
/* 粘贴 */
CControlCommand::CControlCommand(CControlView* parentView, QPoint pos, CShowItem* item, CMDTYPE cmdtype)
{
_pViewContianer = parentView; ;
_Position = pos;
_ShowItem = item;
_eCmdType = cmdtype;
_cmdName = "";
if (_eCmdType == CMDTYPE::PASTE)
{
_cmdName = "粘贴控件";
}
if (nullptr != _ShowItem)
{
_ItemType = _ShowItem->ItemType();
}
_eStatus = RUNSTATUS::NotRun;
_NewShowItem = nullptr;
cmdResStatus = false;
}
/* 执行新建控制指令 */
bool CControlCommand::Execute()
{
bool ret = true;
SetCmdStatus(RUNSTATUS::Running);
if (nullptr != _pViewContianer)
{
switch (_eCmdType)
{
case CControlCommand::CREATE:
_ShowItem=_pViewContianer->CreateWidget(_Position,_ItemType); /* 创建控件 */
if (nullptr== _ShowItem)
{
ret = false;
}
break;
case CControlCommand::COPY:
if (nullptr!=_ShowItem)
{
_pViewContianer->SetCopyItem(_ShowItem);
}
break;
case CControlCommand::PASTE:
if (nullptr != _ShowItem)
{
_NewShowItem =_pViewContianer->CopyCreateWidget(_Position, _ShowItem);
if (nullptr== _NewShowItem)
{
ret = false;
}
}
break;
case CControlCommand::REMOVE:
if (nullptr != _ShowItem)
{
_pViewContianer->DeleteWidget(_ShowItem);
}
break;
default:
break;
}
}
/* 指令执行结果 */
cmdResStatus = ret;
SetCmdStatus(RUNSTATUS::NotRun);
//qDebug() << "执行指令 :"<<_cmdName<<" 执行结果: "<< ret;
return ret;
}
/* 执行新建状态 */
bool CControlCommand::IsEnabled()
{
bool ret = true;
if (GetStatus()== RUNSTATUS::Running)
{
ret = false;
}
return ret;
}
/* 重做指令 */
void CControlCommand::Redo()
{
CControlCommand::Execute();
qDebug() << "重做指令 :" << _cmdName << " 重做类型: " << _eCmdType;
}
/* 撤销指令 */
void CControlCommand::Undo()
{
bool ret = true;
SetCmdStatus(RUNSTATUS::Running);
if (nullptr != _pViewContianer)
{
switch (_eCmdType)
{
case CControlCommand::CREATE: /* 创建撤销 ,将创建对象移除*/
if (nullptr != _ShowItem)
{
_Position = _ShowItem->GetPosition();
_pViewContianer->DeleteWidget(_ShowItem);
}
break;
case CControlCommand::COPY: /* 复制对象撤销 ,则将复制对象设置为空 */
if (nullptr != _ShowItem)
{
_Position = _ShowItem->GetPosition();
_pViewContianer->SetCopyItemNull();
}
break;
case CControlCommand::PASTE: /* 粘贴对象撤销 */
if (nullptr != _NewShowItem)
{
_Position = _NewShowItem->GetPosition(); /* 更新控件位置 */
_pViewContianer->DeleteWidget(_NewShowItem);
}
break;
case CControlCommand::REMOVE: /* 移除对象撤销*/
if (nullptr != _ShowItem)
{
_Position = _ShowItem->GetPosition(); /* 更新控件位置 */
}
_NewShowItem = _pViewContianer->CopyCreateWidget(_Position, _ShowItem);
if (nullptr == _NewShowItem)
{
ret = false;
}
_ShowItem = _NewShowItem;
_NewShowItem = nullptr;
break;
default:
break;
}
}
qDebug() << "撤销指令 :" << _cmdName << " 撤销类型: " << _eCmdType;
SetCmdStatus(RUNSTATUS::NotRun);
}
4.2 控制指令管理类
控制指令管理类主要实现控制指令的添加、执行、撤销及重做的控制。具体实现思路再3.2.2节中已进行了简要说明。通常,撤销与重做次数都是有限的,如果无限制的话容易造成软件异常,通常可支持最近的10步、20步或者50步撤销,具体参数可自行决定,支持撤销的步数即为控制指令队列的大小容量。当指令队列满队时,将队头的第一个指令移除,将新增指令添加到队尾。具体实现思路如下。
CCommandManager.h头文件示意如下,由于指令管理类需要对软件所有指令进行控制,因此采用单例模式将其封装。
#define MAX_CMD_CACHE_STEP 10 /* 指令缓存最大数量*/
class CCommandManager
{
public:
static CCommandManager* GetInstance()
{
static CCommandManager instance;
return &instance;
}
~CCommandManager();
/* 运行指令 */
void RunCmd(CControlCommand * cmd);
/* 刷新 */
void Refresh();
/* 撤销 */
void Undo();
/* 重做 */
void Redo();
/* 获取撤销与使能状态 */
bool GetRedoEnable();
bool GetUndoEnable();
protected:
void AddCmd(CControlCommand * cmd);
void RemoveCmd(CControlCommand * cmd);
private:
std::vector<CControlCommand*> _CmdVec; /* 指令队列 */
std::vector<CControlCommand*>::iterator _CmdVecIt; /* 指令指针 */
bool _RedoEnable; /* 重做使能 */
bool _UndoEnable; /* 撤销使能 */
};
CCommandManager.cpp实现文件示意如下,重点为队列满队时添加指令、指令是否可撤销与重做的判断,具体原则为:
- 当队列满队时,先将队头元素出队,再将新指令添加至队尾;
- 当指令撤销至队头时则无法再执行撤销重做,当指令重做至队尾时则无法再执行重做。
/* 析构函数 */
CCommandManager::~CCommandManager()
{
}
/* 运行指令 */
void CCommandManager::RunCmd(CControlCommand * cmd)
{
bool isval = cmd->IsEnabled(); /* 指令是否有效*/
if (!isval)
{
qDebug() << " 当前指令无效:" << cmd->GetCmdName();
return;
}
/* 判断是否有执行指令 */
for (size_t i = 0; i < (int)_CmdVec.size(); i++)
{
CControlCommand* tempCmd = _CmdVec.at(i);
auto status = tempCmd->GetStatus();
if (status == CControlCommand::RUNSTATUS::Running)
{
qDebug() << " 当前指令与" << cmd->GetCmdName() << " 正在运行指令 " << tempCmd->GetStatus() << " 冲突!";
return;
}
}
/* 将命令放入队列中 */
AddCmd(cmd);
qDebug() << " AddCmd:" << cmd->GetCmdName() << " cmdVec Size" << _CmdVec.size();
/* 执行指令 */
cmd->Execute();
/* 设置当前活动指令 */
_CmdVecIt = _CmdVec.end() - 1;
//qDebug() << "当前迭代器 _CmdVecIt:" << (*_CmdVecIt)->GetCmdName();
}
/* 刷新 */
void CCommandManager::Refresh()
{
}
/* 撤销 */
void CCommandManager::Undo()
{
if (_CmdVec.size() == 0 || _UndoEnable == false)
{
qDebug() << "当前无可撤销的步骤!";
return;
}
auto it = _CmdVecIt;
(*it)->Undo();
if (_CmdVec.begin() != _CmdVecIt)
{
_CmdVecIt = it - 1;
}
else
{
_UndoEnable = false;
}
_RedoEnable = true;
qDebug() << "撤销步数序号 :" << (int)(_CmdVec.end() - it);
}
/* 重做 */
void CCommandManager::Redo()
{
if (_CmdVec.size() == 0 || _RedoEnable == false)
{
qDebug() << "当前无可重做的步骤!";
return;
}
auto it = _CmdVecIt;
(*it)->Redo();
if (_CmdVecIt != (_CmdVec.end() - 1)) //迭代器
{
_CmdVecIt = it + 1;
}
else //迭代器在最后一步
{
_RedoEnable = false;
}
_UndoEnable = true;
qDebug() << " 重做步数序号 :" << (int)(_CmdVec.end() - it);
}
/* 添加指令 */
void CCommandManager::AddCmd(CControlCommand * cmd)
{
if (_CmdVec.size() == MAX_CMD_CACHE_STEP)
{
RemoveCmd(*_CmdVec.begin()); /*移除队列中第一个指令*/
}
_CmdVec.emplace_back(cmd);
_RedoEnable = true;
_UndoEnable = true;
}
/* 移除指令 */
void CCommandManager::RemoveCmd(CControlCommand * cmd)
{
std::vector<CControlCommand *>::iterator iterTemp;
for (iterTemp = _CmdVec.begin(); iterTemp != _CmdVec.end(); iterTemp++)
{
if (cmd == *iterTemp)
{
_CmdVec.erase(iterTemp);
break;
}
}
}
/* 获取重做使能状态 */
bool CCommandManager::GetRedoEnable()
{
if (_CmdVec.size() == 0)
{
_RedoEnable = false;
}
return _RedoEnable;
}
/* 获取撤销使能状态 */
bool CCommandManager::GetUndoEnable()
{
if (_CmdVec.size() == 0)
{
_UndoEnable = false;
}
return _UndoEnable;
}
5 总结
本文结合实际案例说明了指令模式在指令参数化、指令撤销与重做方面的应用,由于本案例的初衷为最小程度对代码进行改动,指令调用者与指令接收者并未完全解耦,在实际应用中应尽量结合案例进行设计模式的应用于与简化。
最后,说一下本应用案例中未考虑到内存管理问题,即指未考虑何时释放指令指针。通常,可使用智能指针封装指令,指令内部所有指针变量均需更改为智能指针,在指令队列满队移除指令时,可调用指令销毁函数,内存管理较难,后续研究后再进行总结。
参考文献
[1] 一文讲透设计模式(C++版)
[2] 秦小波.设计模式之禅.[M].北京.机械工业出版社,2010.3[第15章]
[4] 埃里克.伽马,理查德.赫尔穆,著.李英军,…译.设计模式-可复用面向对象软件的基础.河北保定.机械工业出版社,2023.3[第5.2章]