一、概述
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,
¶ms,&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;
}