• 设计模式-命令模式


    命令模式:

    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),重做,历史记录,已经队列化命令都可以了.

  • 相关阅读:
    【loj2639】[Tjoi2017]不勤劳的图书管理员
    【bzoj3514】Codechef MARCH14 GERALD07加强版
    BZOJ1002【FJOI2007】轮状病毒
    数论基础(附加例题)
    表达式总结
    背包
    hdu1027
    hdu1026
    hdu1025
    hdu1024
  • 原文地址:https://www.cnblogs.com/bbsno1/p/3260704.html
Copyright © 2020-2023  润新知