大卫的Design Patterns学习笔记14:Command

一、概述
Command(命令)模式可用于将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,即允许用户指定对何种对象执行何种操作;或者,对请求排队或记录请求日志,以及支持可撤消的操作。

二、结构
Command模式的结构如下图所示:

1、Command模式类图示意
上图中包括如下角色:
客户(Client)角色:创建了一个具体命令 (ConcreteCommand )对象并确定其接收者。
命令(Command)角色:声明了一个给所有具体命令类的抽象接口。这是一个抽象角色。
具体命令(ConcreteCommand)角色:定义一个接受者和行为之间的弱耦合;实现Execute ()方法,负责调用接收考的相应操作。Execute ()方法通常叫做执方法。
请求者(Invoker)角色:负责调用命令对象执行请求,相关的方法叫做行动方法。
接收者(Receiver)角色:负责具体实施和执行一个请求。任何一个类都可以成为接收者,实施和执行请求的方法叫做行动方法。
从Command模式的结构似乎可以看出几分Adapter模式的影子,确实如此,Invoker通过Command对Receiver的Adptee来Execute Receiver ::Action,但Command模式的意义不在于此,Command模式的目的在于通过将需要操作的对象及所执行的操作封装成一个独立的对象,而不在于简单的接口的转换;此外,二者还有一个显著的区别在于Adaptee对client往往是不可见的,而Receiver对Client往往是可见的。Client只知道Adapter ::Request (),而不知(或无需知道)其内部其实调的是Adaptee ::SpecialRequest (),而在Command模式中Client往往需要显式地把一个Receiver对象传给一个ConcreteCommand对象,以使其能调用Receiver ::Action ()

三、应用
在下面的情况下应当考虑使用命令模式:
1
、使用命令模式作为 "CallBack"在面向对象系统中的替代。 "CallBack"讲的便是先将一个函数登记上,然后在以后调用此函数。
2
、需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命期。换言之,原先的请求发出者可能已经不在了,而命令对象本身仍然是活动的。这时命令的接收者可以是在本地,也可以在网络的另外一个地址。命令对象可以在串形化之后传送到另外一台机器上去。
3
、系统需要支持命令的撤消 (undo )。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用undo ()方法,把命令所产生的效果撤销掉。命令对象还可以提供redo ()方法,以供客户端在需要时,再重新实施命令效果。
4
、如果一个系统要将系统中所有的数据更新到日志里,以便在系统崩溃时,可以根据日志里读回所有的数据更新命令,重新调用Execute ()方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更新。
5
、一个系统需要支持交易 (Transaction )。一个交易结构封装了一组数据更新命令,使用命令模式来实现交易结构可以使系统增加新的交易类型。

四、优缺点
Command模式允许请求的一方和接收请求的一方能够独立演化,从而且有以下的优点:
Command模式使新的命令很容易地被加入到系统里。
允许接收请求的一方决定是否要否决(Veto)请求。
能较容易地设计一个命令队列。
可以容易地实现对请求的Undo和Redo。
在需要的情况下,可以较容易地将命令记入日志。
Command模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。
Command类与其他任何别的类一样,可以修改和推广。
你可以把Command对象聚合在一起,合成为Composite Command。比如宏Command便是Composite Command的例子。
由于加进新的具体命令类不影响其他的类,因此增加新的具体命令类很容易。

Command模式的缺点如下:
使用Command模式会导致某些系统有过多的具体Command类。某些系统可能需要几十个,几百个甚至几千个具体Command类,这会使Command模式在这样的系统里变得不实际。

五、举例
1
、JDK的AbstractUndoableEdit为我们提供了基本的Undo /Redo支持,当我们需要为Java应用引入Undo /Redo操作时,只需简单地从AbstractUndoableEdit类派生子类将操作封装成类即可。下面是一个简单的封装Adjust操作的例子(代码取自XModeler,Java Code):

public class
 SelectTool
    extends AbstractTool  {
    //...

    public
 void mouseDragged (MouseEvent e ) {
        //...
        if  (!isDragRegistered ) {
            desk .addUndoableEdit ( new AdjustUndoableEdit (desk , (ModelObject ) oldC ));
            isDragRegistered  =  true ;
        }
    }
}


public class
 MainFrame extends JFrame implements PropertyChangeListener     // Invoker, the Main Window
{
    //...
    protected  void commandRedo () {
        // Find active child window
        JInternalFrame internalframe  =  this .getActiveChild ();

        // notify the active view to execute the command
        if  (internalframe  != null  && internalframe instanceof ChildFrame ) {
            ChildFrame child  = (ChildFrame ) internalframe ;
            ModelView view  = child .getView ();
            view .redo ();
        }
    }
}


public class
 AdjustUndoableEdit extends AbstractUndoableEdit  {
    ModelObject com ;     // state
    Desktop desk ;             // Receiver, the Active View
    Rectangle oldRec ;

    public
 AdjustUndoableEdit (Desktop desk , ModelObject com ) {
        this
.com  = com ;
        this
.desk  = desk ;
        oldRec  = getBounds ();
    }


    Rectangle getBounds () {
        Rectangle rec  = ( (Component ) com ).getBounds ();
        return
 rec ;
    }


    public
 String getUndoPresentationName () {
        return
 "Undo_Adjust" ;
    }


    public
 String getRedoPresentationName () {
        return
 "Redo_Adjust" ;
    }


    public
 void undo () throws CannotUndoException  {     // Execute 1
        super .undo ();
        Rectangle newRec  = getBounds ();
        ((
Component )com ).setBounds (oldRec );
        oldRec  = newRec ;
        desk .fireAssociatorChanged ();
    }


    public
 void redo () throws CannotRedoException  {     // Execute 2
        super .redo ();
        Rectangle newRec  = getBounds ();
        ((
Component )com ).setBounds (oldRec );
        oldRec  = newRec ;
        desk .fireAssociatorChanged ();
    }
}


上面的代码片断,虽然不是一个完整的Command模式的应用,但其中已经可以十分清晰地看到各个Role,以及他们之间的协作关系。

六、更进一步
在如何将执行请求封装为Command方面,STL的functor(或称function objects)给了我们很多启示(当然functor并非STL首创,但STL使functor为更多人所熟知,因为在使用STL算法的过程中几乎不可避免要用到functor),STL的functor通过将操作封装成 struct / class来实现操作的对象化,但STL设计functor不是为了支持所谓的Command模式,而是作为完整的模板库体系的一部分,与STL算法结合(另一个与STL算法结合使之发挥巨大功效的STL元素是iterator),因为,普通的函数指针很难向上提供统一的接口,而functor通过实现统一的 operator  ()为算法提供了统一的接口。由于主要为STL算法服务,STL只提供了两种简单的functor类型:unary_function和binary_function,你要自己实现用于STL算法的functor往往也需要从这两种functor类型派生,以遵循既定的约定,虽然,在一般的情况下,不这么做并不会出错(只有在使用binder1st /binder2nd等时才会报错)。而对于在普通应用中使用functor的情况,并不需要遵循上面的原则。
STL中unary_function和binary_function的定义如下(在functional中):

template
 < class Arg ,  class Result >
struct
 unary_function  {
    typedef
 Arg argument_type ;
    typedef
 Result result_type ;
};


template
 < class Arg1 ,  class Arg2 ,  class Result >
struct
 binary_function  {
    typedef
 Arg1 first_argument_type ;
    typedef
 Arg2 second_argument_type ;
    typedef
 Result result_type ;
};


以下是一个典型的functor的定义(在functional中):

template
 < class _Ret ,  class _Tp >
class
 mem_fun_t  :  public unary_function <_Tp *,_Ret > {
public
:
  explicit
 mem_fun_t (_Ret  (_Tp ::*__pf )()) : _M_f (__pf ) {}
  _Ret  operator ()(_Tp * __p )  const  {  return  (__p ->*_M_f )(); }
private
:
  _Ret  (_Tp ::*_M_f )();
};


该模板类是模板类mem_fun的adaptee类之一,负责对成员函数进行封装,有了mem_fun,我们就可以方便地写出下面的代码:

#include <vector>
#include <iostream>
#include <functional>
#include <algorithm>
using namespace std ;

struct
 CBase
{

    virtual
 int func () =  0 ;
};


struct
 CDerived1  :  public CBase
{

    int
 func () {cout  <<  this  <<  "/tCDerived1::func"  << endl ;  return  0 ;}
};


struct
 CDerived2  :  public CBase
{

    int
 func () {cout  <<  this  <<  "/tCDerived2::func"  << endl ;  return  0 ;}
};


int
 main ()
{

    vector <CBase *> v_a ;
    CDerived1 d1 ;
    CDerived2 d2 ;

    v_a .push_back (&d1 );
    v_a .push_back (&d2 );

    for_each (v_a .begin (), v_a .end (), mem_fun (&CBase ::func ));

    return
 0 ;
}


但STL的mem_func有个明显的问题,它是个unary_function,也就是说,只能用它来封装不带参数(并且返回类型不是 void,这是由mem_fun的 operator  ()的定义决定的)的成员函数。
与STL functor专属于STL算法不同,Loki库实现了一个通用的Functor模板类,该模板库可以支持最多 15个参数,而与之类似的C ++社区的明星boost库的function则可以支持最多 50个参数。由于Loki ::Functor的内部实现涉及TYPELIST等相关知识,不可能在此通过三言两语解释清楚,感兴趣的朋友可以参考Loki库的实现者Andrei . A所著Modern C ++ Design一书,或Loki库源码;boost ::function的实现原理可以参考 <...>

下面提供一个运用Loki ::Functor实现Command模式的例子(你可以很容易地将其改成使用boost ::function或普通functor,个人认为,与使用普通functor类相比,使用Loki ::Functor或boost ::function的好处仅在于可以方便灵活地使用functor,而无需将类改造成functor类,即实现 operator  (),而且,可以方便地指定在对象上执行某个函数;当然,Loki ::Functor和boost ::function作为模板类,有其独特的优势--复用实现,但在下面的例子中这并不是一个需要考虑的问题) :

#include <iostream>
#include <vector>
#include <Functor.h>
#include <SmallObj.cpp>
using namespace std ;
using namespace
 Loki ;

struct
 Receiver
{

    virtual
 bool Action ( int ,  char *) =  0 ;
};


struct
 ConcreteReceiver1  :  public Receiver
{

    bool
 Action ( int ,  char *)
    {

        cout  <<  "ConcreteReceiver1::Action"  << endl ;
        return
 true ;
    }
};


struct
 ConcreteReceiver2  :  public Receiver
{

    bool
 Action ( int ,  char *)
    {

        cout  <<  "ConcreteReceiver2::Action"  << endl ;
        return
 true ;
    }
};


struct
 Invoker
{

    vector <Functor < bool , TYPELIST_2 ( int ,  char *)>*> v_functor ;

    void
 AddCommand (Functor < bool , TYPELIST_2 ( int ,  char *)>* p_command )
    {

        v_functor .push_back (p_command );
    }
};


typedef
 Invoker CommandManager ;

int
 main ()
{

    CommandManager cm ;
    ConcreteReceiver1 rcv1 ;
    ConcreteReceiver2 rcv2 ;

    Functor < bool , TYPELIST_2 ( int ,  char *)>
        cmd1 (&rcv1 , &Receiver ::Action ),
        cmd2 (&rcv2 , &Receiver ::Action );

    cm .AddCommand (&cmd1 );
    cm .AddCommand (&cmd2 );

    // Some procedure...

    (*
cm .v_functor [ 0 ])( 1 ,  "A" );
    (*
cm .v_functor [ 1 ])( 2 ,  "B" );

    cmd1 ( 1 ,  "A" );
    cmd2 ( 2 ,  "B" );

    return
 0 ;
}


由于上述例子中只需要简单地对调用请求进行转发,其中没有了确切的Command及其子类ConcreteCommand,所有Command类被完全封装在各Functor对象中,你可以对其进行二次封装,将Functor对象改成成员变量,从而更好地控制执行过程。
对于上面的例子,不论采用何种封装Command的方式,对于可变参数都显得有点力不从心,因为对于程序设计来讲,可变参数始终是件令人头疼的事情,虽然有va_list /va_start /va_arg /va_end等辅助,但是参数类型检查呢?这里Reflect(反射)机制可以在一定程度上解决我们的问题。COM中IDispatch ::Invoke和Java的Method ::invoke就是Reflect的典型应用,借助反射机制,我们可以将参数封装成数组的形式,使得我们的Command可以对上层保持统一的接口形式(一个参数数组  + 一个返回值)。在某些情况下,boost ::any也可以用于参数传递,但由于boost ::any会在传递数据时丢失参数的类型信息,所以,如果要使用boost ::any实现类似Reflect的效果,需要自己处理和保存类型信息(像IDispatch ::Invoke使用DISPPARAMS一样)。

附注:
1.
Java Reflect机制示例
import java .util .*;
import java .lang .reflect .*;

public class
 Command
   {

   private
 Object receiver ;
   private
 Method command ;
   private
 Object [] arguments ;

   public
 Command (Object receiver , Method command ,
                           Object [] arguments  )
      {

      this
.receiver  = receiver ;
      this
.command  = command ;
      this
.arguments  = arguments ;
      }

   public
 void execute () throws InvocationTargetException ,
                                 IllegalAccessException
      {

      command .invoke ( receiver , arguments  );
      }
   }


public class
 Test  {
   public static
 void  main (String [] args ) throws Exception
      {

      Vector sample  =  new Vector ();
      Class [] argumentTypes  = { Object . class  };
      Method add  =
         Vector . class .getMethod (  "addElement" , argumentTypes );
      Object [] arguments  = {  "cat"  };

      Command test  =  new Command (sample , add , arguments  );
      test .execute ();
      System .out .println ( sample .elementAt (  0 ));
      }
   }


2.
COM IDispatch ::Invoke示例

::
CoInitialize (NULL );
HRESULT hr ;
IDispatch  *pDispatch =NULL ;
try

{

    CLSID clsid ;
    hr =::CLSIDFromProgID ( L"DispDll.Fun" ,&clsid );
    if
(FAILED (hr ))     throw ( 0 );

    hr =::CoCreateInstance (clsid ,NULL ,CLSCTX_SERVER ,
        IID_IDispatch ,(LPVOID  *)&pDispatch );
    if
(FAILED (hr ))     throw ( 0 );

    OLECHAR  *arrFunName []={ L"Add" };
    DISPID dispID ;
    hr =pDispatch ->GetIDsOfNames (IID_NULL ,arrFunName , 1 ,LOCALE_SYSTEM_DEFAULT ,&dispID );
    if
(FAILED (hr ))     throw ( 0 );
    VARIANT v [ 2 ];
    v [ 0 ].vt =VT_I4 ;    v [ 0 ].lVal = 3 ;     // parameter 2
    v [ 1 ].vt =VT_I4 ;    v [ 1 ].lVal = 2 ;     // parameter 1, if we use CComDispatchDriver::Invoke, we need not put parameters in reverse.
    DISPPARAMS params ={v ,NULL , 2 , 0 };
    // equals to the following 4 lines
/*    params.rgvarg=v;
    params.rgdispidNamedArgs=NULL;
    params.cArgs=2;
    params.cNamedArgs=0;
*/

    VARIANT vResult ;
    hr =pDispatch ->Invoke (dispID ,IID_NULL ,LOCALE_SYSTEM_DEFAULT ,DISPATCH_METHOD ,
            &
params ,&vResult ,NULL ,NULL );
    if
(FAILED (hr ))     throw ( 0 );
    CString s ;    s .Format ( "%d" ,vResult .lVal );
    AfxMessageBox (s );
    pDispatch ->Release ();
}

catch
(...)
{

    if
(pDispatch )    pDispatch ->Release ();
}
::
CoUninitialize ();

以下是一个典型的Invoke实现(如果你使用ATL的IDispatchImpl,则无需作以下处理),该函数的主要工作是对参数逐一进行解析,并根据传入的DISPID(即Method的编号)将参数填入对应的函数进行处理,当然,还包含一些必要的出错处理:

STDMETHODIMP CDispatchSink ::Invoke (DISPID dispidMember , REFIID riid ,
                                   LCID lcid , WORD wFlags ,
                                   DISPPARAMS * pdispparams , VARIANT *
                                   pvarResult , EXCEPINFO * pexcepinfo ,
                                   UINT * puArgErr )
{

   HRESULT hr  = S_OK ;
   if
 (pdispparams )
   {

      switch
 (dispidMember )
      {

         case
 2 :
         {

            if
 (pdispparams ->cArgs  ==  1 )
            {

               if
 (pdispparams ->rgvarg [ 0 ].vt  == VT_I2 )
                  Event2 (pdispparams ->rgvarg [ 0 ].iVal );
               else

                  hr  = DISP_E_TYPEMISMATCH ;     // parameter type is not desired.
            }
            else

               hr  = DISP_E_BADPARAMCOUNT ;     // parameter count is wrong
            break ;
         }

// Other desired case statements
         default :
         {

            hr  = DISP_E_MEMBERNOTFOUND ;     // dispidMember is not desired
            break ;
         }
      }
   }

   else

      hr  = DISP_E_PARAMNOTFOUND ;
   return
 hr ;
}
现代C++中的设计模式是用于对象重用的可重复性方法。设计模式是一种在不同情况下解决相似问题的经验总结,可以通过将问题解决方案的关键部分抽象出来,从而提供灵活性和可重用性。设计模式不是编程语言特定的功能,而是一种通用的方法论。 在现代C++中,有许多常用的设计模式可以用于对象的可重用性。以下是几个常见的设计模式示例: 1.单例模式:用于确保一个类只能创建一个实例,并提供对该实例的全局访问点。对于有些对象只需要一个实例的情况,单例模式可以确保该实例的唯一性,从而方便访问和管理。 2.工厂模式:用于创建对象的过程中封装创建逻辑,让客户端代码无需关心对象的具体创建细节。通过工厂模式,可以通过一个工厂类来创建对象,从而提供更高的灵活性和可扩展性。 3.观察者模式:用于对象之间的发布-订阅机制,让一个对象(主题)的状态发生变化时,能够通知并自动更新其他依赖于该对象的对象(观察者)。通过观察者模式,可以实现对象之间的松耦合和消息传递,提高对象的可重用性和可维护性。 4.适配器模式:用于将一个类的接口转换成客户端所期望的另一个接口。适配器模式可以解决接口不兼容的问题,从而使得原本不兼容的类能够一起工作,提高可重用性和互操作性。 5.策略模式:用于定义一系列算法/行为,并将其封装成独立的类,使得它们可以互相替换。策略模式可以在运行时根据需要动态切换算法/行为,从而提供更高的灵活性和可重用性。 这些设计模式都是在现代C++中常见且有用的重用性方法,可以根据具体的应用场景选择合适的设计模式来提高代码的可维护性、可扩展性和可重用性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值