对于Command模式,目前我还想不出有比是用遥控器操纵家电这个例子更经典的示例,这个情景出自<Head First Design Pattern>,可以很清晰明了的说明Command模式的概念和使用方法,本文在此凭借自己的记忆重复下这个模式。想学模式的话建议先读<Head First Design Pattern>,然后再看四人帮。
Command模式属于行为型模式,与通常的对事物的封装不同,该模式将行为封装成类,目的是实现对象的请求者和对象接收者之间的解藕,下面对此进行描述,先看一个情景:
每个家庭都有很多家用电器:电视机、VCD、空调等,对于每个特定的对象都有对应的不同的方法来操作他,比如开闭电视需要找到电视机的遥控器然后打开/关闭,操作空调需要找到空调的遥控器然后打开/关闭,同样的、VCD也需要自己的遥控器,如果我们要为这三个电器设计类的话,他们长的就像这样:
图 1
这三个类一点联系都没有,在生活中我们要分别拿着每个电器的遥控器对其进行操作,实现起来也比较简单:无非就是拿起一个遥控器然后执行相应的操作,简单的想,就是你的一个遥控器的开关按钮对应电器的一个打开或关闭的动作、然后你的main函数需要操作哪个电器就创建这个该电器的遥控器来完成操作(三个遥控器的对象TVContoller、VCDController、AirContoller),在这种情况下,假设需要添加一个新的电器,比如电灯泡,那么在代码里增加对其操作的支持就需要修改三个地方:
1,增加灯泡类Bulb;
2,增加灯泡的遥控器BulbController;
3,修改main函数完成对灯泡的操作 --> 如果main要操错灯泡的话。
可见这种设计即没有扩展性、并且会逐渐的增加维护的难度、对其进扩展改容易引起新的Bug。现在我们把遥控器设计的简单一些,在这里假设只有一个遥控器,这也是为了能清楚的阐述Command模式的要求,其设计如下:
图 2
要用这个遥控器来操作这三个电器、电器类设计如图1,其类设计用UML描述如下所述:
图 3
代码大致如下:
using namespace std;
typedef enum
{
DEVICE_TV = 1, // Startup index.
DEVICE_VCD,
DEVICE_AirCondition
}DEVICE_t;
class Television
{
public:
void Open(void)
{
cout << "TV opened" << endl;
}
void Close(void)
{
cout << "TV Closed" << endl;
}
};
class VCD
{
public:
void PowerOn(void)
{
cout << "VCD PowerOn" << endl;
}
void PowerOff(void)
{
cout << "VCD PowerOff" << endl;
}
};
class RemoteController
{
public:
RemoteController(Television& tv, VCD& vcd)
:theTV(tv), theVCD(vcd) // Reference, it can only be initialized here
{
}
public:
void PressOnButton(int index)
{
switch(index)
{
case DEVICE_TV:
theTV.Open();
break;
case DEVICE_VCD:
theVCD.PowerOn();
break;
default:
break;
}
}
void PressOffButton(int index)
{
switch(index)
{
case DEVICE_TV:
theTV.Close();
break;
case DEVICE_VCD:
theVCD.PowerOff();
break;
default:
break;
}
}
private:
private:
Television& theTV;
VCD& theVCD;
};
int main(int argc, char* agrv[])
{
Television tv;
VCD vcd;
RemoteController rc(tv, vcd);
rc.PressOnButton(DEVICE_TV); // Open TV
rc.PressOffButton(DEVICE_TV); // Turn off TV
rc.PressOnButton(DEVICE_VCD); // Open VCD
return 0;
}
运行结果:
TV Closed
VCD PowerOn
到目前为止,我们实现了有一个遥控器对多个家电的控制,如果要增加新的家电比如灯泡我们需要修改三处地方:
1,增加Bulb类;
2,扩展RemoteController类的构造函数并增加switch语句的case项,同时扩展DEVICE的枚举相(因为实际中枚举DEVICE肯定是和RemoteController绑定到一起的,而对电器来说是透明的,因此将其归为一处);
3,main函数里增加一个调用 --> 如果main要操作灯泡的话。
可以看到这个设计同样缺乏扩展性,除了如前所述的维护问题外,更要命的是当遥控器的可操控插槽用完时就没办法继续增加对新事物的支持(在图2中,如果other项被用完之后就无法继续扩展)。在写前面那段代码的时候我都脑子里始终有一个想法,那就是动态的设置当前的操控对象,之所以要实现前面的那段代码,就是想渐进的改进这段代码并且能明确的比较它们的的优劣,动态设置按钮的操控对象,该遥控器及新的UML设计图如下所示:
改进后的遥控器 改进后的类设计
基于该设计的代码实现如下所示
using namespace std;
class IDevice
{
public:
virtual ~IDevice() = 0;
};
IDevice::~IDevice() /** If the abstract class only has the destructor method, then **/
{ /** should provide the definition for the destructor, otherwisse **/
class Television : public IDevice
{
public:
void Open(void)
{
cout << "TV opened" << endl;
}
void Close(void)
{
cout << "TV Closed" << endl;
}
public:
virtual ~Television(){}
};
class VCD : public IDevice
{
public:
void PowerOn(void)
{
cout << "VCD PowerOn" << endl;
}
void PowerOff(void)
{
cout << "VCD PowerOff" << endl;
}
public:
virtual ~VCD(){}
};
class RemoteController
{
public:
RemoteController(IDevice* pdev = 0)
:pDevice(pdev)
{
}
public:
void SetDevice(IDevice* pdev)
{
pDevice = pdev;
}
void PressOnButton()
{
Television* ptv;
VCD* pvcd;
if (ptv = dynamic_cast<Television*>(pDevice))
{
ptv->Open();
}
else if(pvcd = dynamic_cast<VCD*>(pDevice))
{
pvcd->PowerOn();
}
}
void PressOffButton()
{
Television* ptv;
VCD* pvcd;
if (ptv = dynamic_cast<Television*>(pDevice))
{
ptv->Close();
}
else if (pvcd = dynamic_cast<VCD*>(pDevice))
{
pvcd->PowerOff();
}
}
private:
IDevice* pDevice; // 考虑下这里能不能用引用替换指针?why or why not?
};
int main(int argc, char* agrv[])
{
Television tv;
VCD vcd;
RemoteController rc(&tv);
rc.PressOnButton();
rc.PressOffButton();
rc.SetDevice(&vcd);
rc.PressOnButton();
return 0;
}
运行结果:
TV Closed
VCD PowerOn
这段改进的代码相比之前已经有了许多进步,相比前一次的设计,现在我们解决了用一个遥控器控制无数多家电的问题。为了增加动态配置遥控器功能,在RemoteController里增加了一个SetDevice方法,然后引入了一个家电类的接口IDevice,这样做就可以使遥控器只需要一个Device的指针成员pDevice和一个配置家电的方法SetDevice。试想,如果不增加IDevice这个接口,那么RemoteController类就要维护每一个家电的指针并且需要为每一个电器都提供一个SetXXX的函数,并且在设置遥控器操纵一个电器都时候要把其他所有电器的指针置空,如果要对这样的代码进行扩展或删减,那简直就是一个维护的噩梦,也可以用一个成员记录遥控器当前所指的家电,然后在SetXXX的时候只将当前的家电指针置空,但是能好到哪里去呢?所以还是不要继续纠缠在一个糟糕的设计上了。
再回头看看现在的代码,我发现如果要添加一个新的电灯泡,那么我需要这样修改:
Step1,增加灯泡类Bulb;
Step2,修改RemoteController的PressOn()和PressOff()方法
Step3,修改main函数 --> 如果main要操纵这个灯泡的话
这个改动仍然需要在三处进行修改,但是不管怎么设计Step1和Step3都是不可避免的,毕竟一个要使用、一个要被用吗,总不可能凭意念进行操作、代码还是实打实的,OO里所说的使代码的修改对客户透明并不是说的Step1和Step3,而是Step2就是遥控器,但是单就Step2而言,这个设计也不能满足"开闭原则"(Open-Closed Priciple,即代码应该对扩展开放、对修改关闭,具体可见Martin Flower的<极限编程>一书),具体来说就是违反了"对修改关闭",RemoteController类仍然不能避免被改动,在这一点上现在的设计相对于前一次的设计并没有优势,因为同样不能将RemoteController类独立出来,并且动态类型转换(dynamic_cast)还会导致程序的效率降低。
遵循"对修改关闭"的原则,我们试图使代码的扩展不会对一个设计良好的RemoteController类产生影响,即在不改动这个类的情况下也能是程序运行良好,这样RemoteController就不需再跟着外界的变化而变化了,有什么办法呢?看看遥控器,无论如何,RemoterController都必须提供PressOn()和PressOff()这两个方法,如果把这两个方法里的调用委托给一个专门的动作对象让他去执行具体的动作,像这样:
{
onCommand.execute();
}
PressOff()
{
offCommand.execute();
}
这样PressOn和PressOff就解放出来了,这段代码中的onCommand和offCommand给我们三个暗示:
1,RemoteController类需要提供一个设置Command的方法SetCommand;
2,RemoteController类应该有两个数据成员保存针对不同的电器的Command的对象;
3,execute提示我们ICommand接口里至少应该有一个execute方法。
于是设计一个接口ICommand就顺利成章了,就像下面的代码所示
{
public:
virtual void execute() = 0;
};
至此新的设计如图所示:
图 6
下面是Command模式的完整实现:
using namespace std;
/**
* the Command interface
*/
class ICommand
{
public:
virtual void execute() = 0;
};
/**
* The Receiver class
*/
class Television
{
public:
void Open(void)
{
cout << "TV opened" << endl;
}
void Close(void)
{
cout << "TV Closed" << endl;
}
};
class VCD
{
public:
void PowerOn(void)
{
cout << "VCD PowerOn" << endl;
}
void PowerOff(void)
{
cout << "VCD PowerOff" << endl;
}
};
/**
* The concrete command for turning on the TV
*/
class OpenTVCommand :public ICommand
{
public:
OpenTVCommand(Television& tv)
: theTV(tv)
{
}
void execute()
{
theTV.Open();
}
private:
Television& theTV;
};
/**
* The concrete command for turning off the TV
*/
class CloseTVCommand : public ICommand
{
public:
CloseTVCommand(Television& tv)
: theTV(tv)
{ }
void execute()
{
theTV.Close();
}
private:
Television& theTV;
};
class PowerOnVCDCommand : public ICommand
{
public:
PowerOnVCDCommand(VCD& vcd)
: theVCD(vcd)
{
}
void execute()
{
theVCD.PowerOn();
}
private:
VCD& theVCD;
};
class PowerOffVCDCommand : public ICommand
{
public:
PowerOffVCDCommand(VCD& vcd)
: theVCD(vcd)
{
}
void execute()
{
theVCD.PowerOn();
}
private:
VCD& theVCD;
};
/**
* The invoker class
*/
class RemoteController
{
public:
RemoteController(ICommand* pOnCmd = 0, ICommand* pOffCmd = 0)
: pTurnOnCmd(pOnCmd), pTurnOffCmd(pOffCmd)
{
}
public:
void SetCommand(ICommand* pOnCmd, ICommand* pOffCmd)
{
this->pTurnOffCmd = pOnCmd;
this->pTurnOnCmd = pOffCmd;
}
void PressOnButton()
{
if (NULL != pTurnOnCmd)
{
pTurnOnCmd->execute();
}
}
void PressOffButton()
{
if (NULL != pTurnOffCmd)
{
pTurnOffCmd->execute();
}
}
private:
ICommand* pTurnOnCmd;
ICommand* pTurnOffCmd;
};
/**
* The test class or client, which is a good
* sample on how to use Command by Client.
*/
int main(int argc, char* agrv[])
{
/**
* Create Receiver
*/
Television tv;
VCD vcd;
/**
* Create Concrete Command
*/
OpenTVCommand openTvCmd(tv);
PowerOnVCDCommand openVcdCmd(vcd);
PowerOffVCDCommand closeVcdCmd(vcd);
RemoteController rc(&openTvCmd);
rc.PressOnButton();
/* Set command */
rc.SetCommand(&openVcdCmd, &closeVcdCmd);
rc.PressOnButton();
rc.PressOffButton();
return 0;
}
输出结果:
VCD PowerOn
VCD PowerOn
现在,如果我们要再添加对新灯泡支持的话,需要做的修改就是:
Step1,创建灯泡类Bulb
Step2,从ICommand派生两个Bulb的开关动作的具体类TurnOnBulb和TurnOffBulb
Step3,main函数掉用RemoteContrller::SetCommand方法设置要操作的对象
如前所述、Step1和Step3始终是要修改的, 不同的是现在Step2变了,实际上,Step2中的具体类是何Step1绑定到一起的(也就是说实际中是属于一个大模块的),因此可以将Step1和Step2归为一处。
这个设计的优点是将RemoteController类独立出来,使他可以应对潜在的扩展,即使修改Step1和Step3也不会导致它的重新编译。缺点是把以前的每个动作/方法封装成现在的类,可能导致类爆炸,好与坏还是要根据实际需要来断定。另外,在这次的设计中我去掉了IDevice接口,其实也可以留着,作为对家电的一个抽象,很可能将来有用呢,但是至少目前看不出有什么必要,Martin Flower<极限编程>一书告诉我们只有当需要的时候再去做这件事,不要过度设计。
至此,已经完成了Command模式的描述与实现,现在对Command的进行一个总结。
定义:将行为封装成类,目的是实现对象的请求者和对象接收者之间的解藕,在下图中就是Invoker和Receiver的解耦(通过Command接口)
类图:
序列图:
我画的图跟官方给出的图有点出入,那些都是大师级的人物给出来的,应该更合理吧,但是我总认为他没有勾画出一些必要的联系,比如对Invoker调用总是要由某个Client去触发然后才能执行具体的Command(当然这里只是针对这个简单的例子而言,一些设计当然可以实现Invoker自动触发事件,比如说给一个Timer之类的),所以我在图6中也加入了一个Client到RemoteController到箭头以表示他们的联系。
Command模式的高级应用:
对于Command模式,它常应用于取消操作(UnDo)、事务支持(Transaction)、线程池(Thread Pool)、队列请求(Queuing Request)等,具体可参考http://en.wikipedia.org/wiki/Command_pattern,希望有时间的时候能再这方面再写一点文章