设计模式-命令模式

命令模式:

1,将命令的请求者和命令的执行者进行解耦。
2,通过将命令封装成对象,命令对象封装了接收者和要执行的动作
3,将命令封装成对象,命令可以传递,通过在命令对象中添加undo方法,支持撤销。
4,调用者通过调用命令对象的execute方法,执行接收者的具体动作
5,通过使用宏命令,宏命令也是一个命令对象,只是它是一种组合模式的体现。它拥有一个命令对象列表,代表着一堆要执行的命令,也可以支持撤销。
6,实际操作的时候,可以会有聪明的命令对象,他们不把工作委托给接收者(Receiver.action()),而是直接在execute中实现了Receiver.action()的具体行为。
7,调用者可以动态的更改命令对象,实现动态行为
8,命令模式还可以支持日志系统,通过将执行的命令记录到磁盘中(store方法),从而实现在出现系统故障的时候将命令重新加载。
9,命令模式支持undo,redo,队列模式请求(将命令传递到工作线程中,由工作队列从队列中取出命令对象执行),支持宏命令使用组合模式完成。

命令对象:

一般会有Undo,Redo, Execute三个方法,分别表示撤销,重做和执行。命令对象自身负责撤销和重做,因为它是Execute的发起者,它可以通过保存一些状态来以便于Undo操作。
例如下面的AirConditionCommand命令对象,它保存了风力大小以便于Undo操作。

//封装了命令对象
	class Command
	{
	public:
		virtual void Execute();//执行命令
		virtual void Undo();//撤销操作
		virtual void Redo();//重做
	}
	//这个NoCommand对象是为了提供给CommandControl成员的默认行为的,默认操作室DoNothing。
	class NoCommand : public Command
	{
	public:
		void Execute(){}//执行命令
		void Undo(){}//撤销操作
		void Redo(){}/重做
	}




撤销和重做:

当我们在执行命令的时候,很容易出现误操作,这时候撤销功能就非常必要了,像各种文本编辑器都会拥有很强大的撤销功能。采用命令对象可以将已经执行的命令对象放入一个链表中,当需要撤销的时候,取出链表要撤销的命令对象,执行命令对象的Undo就可以了,为了能够实现重做功能,可以将刚取出的命令对象放入另外一个重做链表中,需要重做的时候,从重做链表中取出对象命令对象,调用Redo就OK了。
宏命令:
命令既然是对象,可以采用组合模式将一系列的命令组合起来,形成更强大的命令,这就叫做宏命令。它既可以是一堆普通命令的集合,也可以是普通命令和宏命令的共同组合。想想目录和文件,目录中既有文件又有目录,是一个递归的思想,宏命令也是递归。
以下为宏命令的代码:

关于宏命令:
		Command** parray = new Command*[10];
		for(int i = 0; i < 10; ++i){
			parray[i] = new DetailCommand;
		}
		MacroCommand* p = new MacroCommand(parray, 10);
		宏命令的具体实现:宏命令也是命令,其保存着普通命令的列表,一次性执行多个命令。这是组合模式的体现,
		通过组合现有的命令基本集来实现更强大的功能。
		class MacroCommand : public Command
		{
		public:
			MacroCommand(list<Command*>* pCommandArray){
				this.pCommandArray = pCommandArray;
			}
		public:
			void Execute(){
				list<Command*>::iterator it;
				for(it = m_pCommandArray->begin(); it!= m_pCommandArray->end(); ++it){
					(*it)->Execute();
				}
			}
			void Undo(){
				list<Command*>::iterator it;
				for(it = m_pCommandArray->begin(); it!= m_pCommandArray->end(); ++it){
					(*it)->Undo();
				}
			}
			void Redo(){
				list<Command*>::iterator it;
				for(it = m_pCommandArray->begin(); it!= m_pCommandArray->end(); ++it){
					(*it)->Redo();
				}
			}
		private:
			list<Command*>* m_pCommandArray;
		};





命令的作用对象Receiver:
Light灯有亮灭两种情况,Light灯初始化时是灭的,需要置亮可以调用On方法,需要置灭调用Off方法。针对这个对象我们封装一个命令对象LightCommand,负责对向Light发出的命令进行管理,Light灯就是具体的Receiver对象。代码如下:
//封装了Receiver对象,它是包含具体的动作代码,由命令对象驱动调用它的的执行。如果有很多类似的对象,可以考虑继承于Receiver类。这样所有的具体的Command对象中
//的私有Receiver成员就可以抽象化了。Receiver* m_pReceiver;,外部在new Command的时候负责传入具体的Receiver。本例子不这样用。

class Light
	{
	public
		Light(const String& strName){
			this.m_strName = strName;
			Off();
		}
	public:
		void On(){
			cout<<m_strName<<" On()"<<endl;
		}
		void Off(){
			cout<<m_strName<<" Off()"<<endl;
		}
	private:
		String m_strName;
	}


具体命令对象:

为了保持命令对象的接口统一,LightCommand对象继承于Command接口。通过Execute方法打开Light,Undo关闭Light,Redo再次打开Light。就通过这个类,我们就可以很好地管理对LIght发出的命令。1,创建命令对象LightCommand,传入要控制的Light指针,2,执行调用Execute,3,撤销调用Undo。

//封装了电灯的Command类
	class LightOnCommand : public Command
	{
	public:
		LightOnCommand(Light* pLight){
			this.m_pLight = pLight;
		}
	public:
		void Execute(){
			m_pLight.On();//打开电灯
		}
		void Undo(){
			m_pLight.Off();//关闭电灯
		}
		void Redo(){
			Execute();
		}
	private:
		Light* m_pLight;
	}



保存状态的命令对象AirconditionCommand:

//上文的Light对象比较简单,因此在封装的LightOnCommand对象也比较简单。可能有一些复杂的对象,需要保存对象的一些属性才可以执行撤销或者重做,那这样的话/
//命令对象就稍微复杂了一点,需要保存命令执行前的状态以便于恢复,对于命令有可能一半成功,一半失败的情况需要具体分析。
//例如有一个空调,空调可以调节风力大小,那这个时候命令执行的时候需要保存当前风力大小,当撤销的时候才可以正确恢复大小。

class AirConditionCommand : public Command
	{
	public:
		AirConditionCommand(AirCondition* pAirCondition, int toSetWind){
			this.m_pAirCondition = pAirCondition;
			this.m_nToSetWind = toSetWind;//要设置的风力
		}
	public:
		void Execute(){
			m_nPrevWind = m_pAirCondition.GetCurrentWind();
			m_pAirCondition.SetWind(m_nToSetWind);//设置风力
		}
		void Undo(){
			m_pAirCondition.SetWind(m_PreWind);//设置上一次风力
		}
		void Redo(){
			m_pAirCondition.SetWind(m_nToSetWind);//设置风力
		}
	private:
		AirCondition* m_pAirCondition;
		int m_nPrevWind;//上一次的风力
		int m_nToSetWind;//要设置的风力
	}




命令对象管理者:

客户通过命令对象管理者对命令对象操纵,从而作用于具体的Receiver对象,通过管理者,可以执行撤销和重做。可以动态改变命令,运行时候动态的新增命令和删除现有的命令。一切只需要给CommandControl增加一些操纵接口。下面的CommandControl管理了一个命令对象数组,通过给数组元素赋值来设置和新增命令。两个栈是为了完成撤销和重做操作。

//封装了命令的请求者
	class CommandControl
	{
	public:
		enum {MAX_COMMAND_SIZE=1000}
		CommandControl(){
			for(int i =0; i < MAX_COMMAND_SIZE; ++i){
				m_arrayCommand[i] = new NoCommand();
			}
			//这里我没有考虑内存管理的问题,以及可能抛出的异常,真正的方法应该有一个Init方法负责这样的初始化,以及关于NoCommand对象的删除工作。
			//NoCommand存在的意义是免除每一次都进行判断m_arrayCommand[i]是否为空。
		}
	public:
		void SetCommand(Command* pCom, int slot){//此方法用来预置命令,已经可以实现动态改变。
			this.m_arrayCommand[slot] = pCom;
		}
		void OnButtonPressed(int slot){
			m_arrayCommand[slot].Executed();
			m_stackUndo.push(m_arrayCommand[slot]);
		}
		void UndoButtonPressed(int slot){
			if(!m_stackUndo.IsEmpty()){
				Command* p = m_stackUndo.top();
				m_stackUndo.pop();//从历史表取出栈顶的命令,即刚执行的命令
				
				p.Undo();//执行撤销
				
				m_stackRedo.push(p);//将其压到Redo表中
			}
		}
		void RedoButtonPressed(int slot){
			if(!m_stackRedo.IsEmpty()){
				Command* p = m_stackRedo.top();
				m_stackRedo.pop();//从历史表取出栈顶的命令,即刚执行的命令
				
				p.Redo();//执行重做
				
				m_stackUndo.push(p);//将其压到Undo表中
			}
		}
	private:
		Command m_arrayCommand[MAX_COMMAND_SIZE];//保存这一堆预先设置的命令,初始化时候保存的是NoCommand对象.
		
		stack<Command*> m_stackUndo;//撤销栈,用户执行了一条命令,就压到栈里面,便于用户执行撤销.
		stack<Command*> m_stackRedo;//重新执行栈,用户撤销之后可能撤销错误,要重新执行。
		//这里我不在考虑栈的容量的问题,因为随着命令的继续执行历史表可能会越来越大,这个时候实际上需要设置一个最大容量,继续压栈的话删除栈底(最早压栈的)的命令。
		//同时为了内存管理的方便,在删除的时候同时调用delete删除命令对象的内存。最好在现有的stack再封装一个stack类比较好,做到栈满地话删除最早的一条.
		//对于Redo栈,是用户执行撤销时将命令对象从Undo栈pop出来,然后将其push到Redo栈。对于Redo栈可以不控制大小(因为它是有Undo控制的,仔细思考一下即可理解).
	};




main程序:

//main驱动测试程序
void main()
{
	Light* pLight = new Light("Light");
	Command* pCommand = new LightOnCommand(pLight);
	CommandControl* pControl = new CommandControl();
	pControl.SetCommand(pCommand, 0);
	pControl.OnButtonPressed();
}




上面的代码会有bug,主要是因为我是先写文档再写的代码,文档阐述的是思想,文档的代码仅仅是辅助理解。vs的工程有完成的测试例子,一般都是命令行程序,通过读取用户输入来实现设置命令,执行命令,新增命令,删除命令的功能,命令执行结果直接打印屏幕,便于及时查看结果,如有需要,可以传到blog上。

消息流图:

Client预先设置好各个按钮对应的命令对象,通过按钮触发动作调用CommandControl对应的ButtonPressed方法。每一个ButtonPressed通过命令对象的方式调用具体的Receiver对象来完成工作。
Clinet ----->     CommandControl     ---------------------->            Command                                     -------> Receiver
void OnButtonPressed(){cmd.Executed()}                  void Execute(){ Receiver.DetailAction() }         void DetailAction(){.....}
viud SetCommand(Command* cmd)
根本:将命令的请求者Client和命令的执行者Receiver解耦,通过命令对象传递,由于哟了命令对象的存在,相关的撤销,日志(Load,Store),重做,历史记录,已经队列化命令都可以了.

原文地址:https://www.cnblogs.com/bbsno1/p/3260704.html