回调函数,相信很多人都用过。比如A中定义一个函数,然后A把这个函数指针发给B,B在某个时刻调用这个指针。这就是个回调。其实在过去面向过程语言,比如C语言里面是大量的用到回调函数。就算是现在面向对象的语言,比如JAVA,C#, 有时也还是会用到回调函数。
考虑这么一个例子,有一个加密软件,支持MD5加密和RSA加密。在这个软件的UI界面上有两种方式使用加密:菜单和一个按钮。通过菜单可以对一个文件加密,通过一个按钮也可以加密。使用回调函数机制,我们可以这么做,定义2个函数,比如:Encrypt_MD5() 和Encrypt_RSA()。然后把这2个函数指针传给UI模块,那么当用户通过菜单或者按钮想加密的时候,就可以通过这2个函数指针来调用加密函数。通过回调函数基本可以解决这个问题,但是回调函数是面向过程语言时代的一个产物。现在基本都是OOP时代了,那么在OOP模式下,有什么办法可以替代回调函数呢?可以考虑命令模式。
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或者记录日志,以及支持可撤销的操作。Command(命令)模式是回调机制的一个面向对象的替代品。
GOF设计模式上是这么说的。先来看看命令模式的结构。
结构
参与者
Command:声明命令操作的接口。
ConcreteCommand:具体执行命令的类。这个类是Command类的子类。通常调用者需要把命令作用的接收者传给Command对象。
Client:创建一个具体命令对象并设定它的接收者。
Invoker: 要求该命令执行这个请求。
Receiver:命令相关的接收者。比如加密例子里面的某个文件,加密函数会对这个文件进行加密。
画了一个图来解释基本的调用过程。
基本调用过程
代码示例
写一些测试代码来模拟这个过程。
首先定义一个接收者类:
class CMyFile { public: CMyFile(const std::string& strFile): m_strFile(strFile){} const std::string& GetFilePath() const{return m_strFile;} protected: std::string m_strFile; };
很简单,就是模拟一个文件对象。
命令类:
class CEncryption { public: virtual void Encrypt() = 0; };
定义一个命令基类,里面只有一个纯虚函数Encrypt()。
定义2个CEncryption的子类:
class CEncryption_MD5: public CEncryption { public: CEncryption_MD5(CMyFile* file): m_file(file){} virtual void Encrypt() { cout << "Encrypt file: " << m_file->GetFilePath() << " with MD5\n"; } protected: CMyFile* m_file; }; class CEncryption_RSA: public CEncryption { public: CEncryption_RSA(CMyFile* file): m_file(file){} virtual void Encrypt() { cout << "Encrypt file: " << m_file->GetFilePath() << " with RSA\n"; } protected: CMyFile* m_file; };
实现具体的加密功能。注意这2个子类都有一个成员来存放接收者。这个例子里面都存储了CMyFile的对象,实际上这里可以根据需要存储不同的接收者。
模拟一个Invoker:
//模拟一个UI类 enum MYCMD{MD5 = 0, RSA}; class CMyUI { public: void StoreCommand(MYCMD cmdtype, CEncryption* cmd) { m_mapCmd[cmdtype] = cmd; } void EncryptFileWithMD5() { CEncryption* cmd = GetCmd(MD5); if (cmd) { cmd->Encrypt(); } } void EncryptFileWithRSA() { CEncryption* cmd = GetCmd(RSA); if (cmd) { cmd->Encrypt(); } } CEncryption* GetCmd(MYCMD cmdtype) { return m_mapCmd[cmdtype]; } protected: std::map<int, CEncryption*> m_mapCmd; };
这个invoker里面保存了各种命令对象,用一个std::map来存放。例子假设只有2个命令:MD5加密和RSA加密。
看看如何使用:
void Pattern_Command() { //模拟invoker CMyUI ui; //创建一个MD5加密命令对象 CMyFile* file = new CMyFile("c:\\test.txt"); CEncryption* cmd = new CEncryption_MD5(file); //创建一个RSA加密命令对象 CEncryption* cmd2 = new CEncryption_RSA(file); //将命令对象送给invoker。 ui.StoreCommand(MD5, cmd); ui.StoreCommand(RSA, cmd2); //模拟调用,比如用户选择MD5加密 ui.EncryptFileWithMD5(); //rsa加密 ui.EncryptFileWithRSA(); delete cmd; delete cmd2; delete file; }
Client会创建命令,然后传给Invoker,那么当用户点击菜单或者按钮的时候,Invoker就可以根据不同的命令找到相应的命令对象,然后调用命令对象的成员函数来实现加密功能。
这是一个最最简单的命令模式例子。那么跟回调函数相比,有什么好处呢?我觉得:
1. 使用了OOP的技术,使得封装性好很多,更加模块化;
2. 可以在命令对象里面保存一些上下文信息,比如支持取消命令等;
3. 扩展新命令比较容易;
4. 可以通过各种命令对象,组合成一个复杂命令,比如混合加密RSA-MD5,先进行RSA加密再进行MD5加密。
复杂命令(也有叫宏命令的)
我们已经有了RSA加密命令和MD5加密命令。那么就可以使用这2个命令来实现混合加密RSA-MD5。
只要新增加一个命令类,然后这个命令类的接收者是RSA命令对象和MD5命令对象,如:
//先RSA再MD5 class CEncryption_RSAMD5: public CEncryption { public: CEncryption_RSAMD5(CEncryption* rsa, CEncryption* md5): cmdRSA(rsa), cmdMD5(md5){} virtual void Encrypt() { cmdRSA->Encrypt(); cmdMD5->Encrypt(); } protected: CEncryption* cmdRSA; CEncryption* cmdMD5; };
CEncryption_RSAMD5不再需要CMyFile的对象了,因为rsa和md5命令对象里面已经有这个文件的接收者了。使用例子:
//混合加密 CEncryption_RSAMD5* cmd3 = new CEncryption_RSAMD5(cmd2, cmd); ui.StoreCommand(RSAMD5, cmd3); ui.EncryptFileWithRSAMD5();
OK, 组合现有的命令对象还是蛮简单的。
仔细看看复杂命令(宏命令)的实现,是不是感觉很熟悉?CEncryption_RSA, CEncryption_MD5和CEncryption_RSAMD5都继承与CEncryption,然后CEncryption_RSAMD5又使用了CEncryption_RSA和CEncryption_MD5的实例。也就是说CEncryption_RSAMD5是由CEncryption_RSA和CEncryption_MD5组成的。这不就是组合模式吗?是的,没错,通常宏命令是用结构型模式里面的Composite(组合)模式来实现的。
取消命令
上面的例子没有考虑取消命令的情况。其实在命令模式里面是可以支持取消命令的。我们知道MD5是一种不可逆的加密算法。那么MD5加密后,怎么再得到原来的内容呢?解密显然不行,因为MD5不可逆。我们可以考虑支持取消命令。比如在MD5加密前,保存原始数据,然后在加密。数据可以考虑保存在MD5命令对象里面或者Invoker里面,具体视情况而定。至于怎么保存,有很多方法,这里不再探讨。其实我个人感觉保存现场来支持取消命令,并不是那么容易,因为有时数据保存还是有难度的。但是起码命令模式给取消命令提供了一种可能。
最后:
个人感觉,当回调函数大量被使用的时候,可以考虑命令模式,看看是否能给我们带来好处。还是那句话,不要死搬硬套设计模式,这样反而适得其反。在引入一个设计模式前,先多想想是否有必要。
付完整代码:
#pragma once #include <string> #include <iostream> #include <map> using namespace std; class CMyFile { public: CMyFile(const std::string& strFile): m_strFile(strFile){} const std::string& GetFilePath() const{return m_strFile;} protected: std::string m_strFile; }; class CEncryption { public: virtual void Encrypt() = 0; }; class CEncryption_MD5: public CEncryption { public: CEncryption_MD5(CMyFile* file): m_file(file){} virtual void Encrypt() { cout << "Encrypt file: " << m_file->GetFilePath() << " with MD5\n"; } protected: CMyFile* m_file; }; class CEncryption_RSA: public CEncryption { public: CEncryption_RSA(CMyFile* file): m_file(file){} virtual void Encrypt() { cout << "Encrypt file: " << m_file->GetFilePath() << " with RSA\n"; } protected: CMyFile* m_file; }; //先RSA再MD5 class CEncryption_RSAMD5: public CEncryption { public: CEncryption_RSAMD5(CEncryption* rsa, CEncryption* md5): cmdRSA(rsa), cmdMD5(md5){} virtual void Encrypt() { cmdRSA->Encrypt(); cmdMD5->Encrypt(); } protected: CEncryption* cmdRSA; CEncryption* cmdMD5; }; //模拟一个UI类 enum MYCMD{MD5 = 0, RSA, RSAMD5}; class CMyUI { public: void StoreCommand(MYCMD cmdtype, CEncryption* cmd) { m_mapCmd[cmdtype] = cmd; } void EncryptFileWithMD5() { CEncryption* cmd = GetCmd(MD5); if (cmd) { cmd->Encrypt(); } } void EncryptFileWithRSA() { CEncryption* cmd = GetCmd(RSA); if (cmd) { cmd->Encrypt(); } } void EncryptFileWithRSAMD5() { CEncryption* cmd = GetCmd(RSAMD5); if (cmd) { cmd->Encrypt(); } } CEncryption* GetCmd(MYCMD cmdtype) { return m_mapCmd[cmdtype]; } protected: std::map<int, CEncryption*> m_mapCmd; };
调用:
void Pattern_Command() { //模拟invoker CMyUI ui; //创建一个MD5加密命令对象 CMyFile* file = new CMyFile("c:\\test.txt"); CEncryption* cmd = new CEncryption_MD5(file); //创建一个RSA加密命令对象 CEncryption* cmd2 = new CEncryption_RSA(file); //将命令对象送给invoker。 ui.StoreCommand(MD5, cmd); ui.StoreCommand(RSA, cmd2); //模拟调用,比如用户选择MD5加密 ui.EncryptFileWithMD5(); //rsa加密 ui.EncryptFileWithRSA(); //混合加密 CEncryption_RSAMD5* cmd3 = new CEncryption_RSAMD5(cmd2, cmd); ui.StoreCommand(RSAMD5, cmd3); ui.EncryptFileWithRSAMD5(); delete cmd; delete cmd2; delete cmd3; delete file; }