回调函数,相信很多人都用过。比如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 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
#include
#include
调用:
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;
}