C/C++ 思考:策略模式在协议解析中的应用
引出问题
在基于消息包的通信协议中,通常会通过一个id或命令名来标识该消息包,程序需要根据不同的标识进行不同的解析策略,提取出想要的内容。例如,一个典型的FTP请求命令是这样的:
USER anonymous\r\n
其中,"USER"是请求命令名,"anonymous"是该命令跟着的参数,"\r\n"是一条FTP命令结尾。
对应FTP请求格式:
命令<SP>参数<CRLF>
传统解析方式
如何从一条FTP请求命令中,解析出命令内容呢?
最直接地,根据空格字符(' ')将请求命令的字符串切分成两部分:命令名、命令参数。然后,将解析出的命令名与已知支持的命令名进行比较,如果匹配上,就调用对应的命令处理程序;如果没匹配上,就调用未知命令异常处理。
于是,我们写出如下代码,对FTP请求命令进行解析:
void handleCommand(const std::string& ftpText)
{
std::string name; // 命令名
std::string arg; // 命令参数
[name, arg] = str_split(ftpText, ' '); // 自定义切分函数, 将ftpText按空格切分成2个子字符串
if (name == "USER") {
handleUSER(arg);
}
else if (name == "PASS") {
handlePASS(arg);
}
else if (name == "CWD") {
handleCWD(arg);
}
...
else {
// Unkown command
handleUnknownCommand(arg);
}
}
策略模式
上面代码能正常运行,但存在2个问题:
1)太繁琐,看起来很糟糕,实际上处理逻辑很简单,都是匹配到命令名就调用对应处理函数;
2)如果要添加对新命令的支持,就要到代码中去添加修改,如果涉及到多个命令,可能出错;
既然都是 “匹配命令 => 调用命令处理程序” 这种模式,能不能将这部分固定下来,而将可能变动的命令名、命令处理函数抽象出来?
答案是可以的,可以用策略模式进行简化。
先复习一下策略模式。
简介
策略模式是一种对象行为模式。
意图:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
适用场景:
1)许多相关的类,仅仅是行为不同,比如一种策略对应一个类。
2)需要使用一个算法的不同变体。
3)算法使用客户不应该知道的数据。
4)一个类定义多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。
策略模式,特别适合用来解决if-else / switch 条件数目不确定的情况,便于用户扩展新的情形。
UML类图
UML类图如下:
其中,
-
Strategy(策略)
定义所有支持的算法的公共接口。Context用该接口调用某个具体的算法ConcreteStrategy。 -
ConcreteStrategy(具体策略)
实现Stragegy接口的某个具体算法。 -
Context(上下文)
使用ConcreteStrategy对象,利用对应算法解决问题的上下文。
改进1:基于函数的代码结构改进
根据策略模式,我们用一个结构体将命令名、命令处理函数用结构体包装起来,然后将支持的命令名、处理函数放到一个数组中。改进代码如下:
typedef struct {
std::string name;
void (*handler)(const std::string&);
} FtpCommand_t;
#define COMMANDS_ARRAY_SIZE 10 // 根据实际情况决定数组大小
static FtpCommand_t commands[COMMANDS_ARRAY_SIZE] = {
{"USER", handleUSER},
{"PASS", handlePASS},
{"CWD", handleCWD},
...
};
void handleCommand(const std::string& ftpText)
{
std::string name; // 命令名
std::string arg; // 命令参数
[name, arg] = str_split(ftpText, ' ');
for (int i = 0; i < COMMANDS_ARRAY_SIZE; ++i) {
if (commands[i].name == name) { // 命令名匹配
commands[i].handler(arg); // 回调命令处理函数
break;
}
}
}
当要添加、修改或删除一个命令及处理时,就可以直接在支持的命令数组commands[]中修改,而不用修改命令名匹配、回调代码。
注意:这种方式并不会提高程序运行效率,但会让程序结构清晰、易懂,维护更简易。
问题:为何改进中,没有任何策略类,却可称作策略模式?
策略模式的核心,是定义一系列算法,根据不同case,来选择使用不同的、可相互替换的算法。至于每个算法,是用类的形式,还是函数的形式,并非关键。而且,在C++中,函数也可以看作一种特殊的类(函数类)。
上面代码中,每种FTP命令,都对应了一种具体的解析策略,或称算法。策略模式的关键,是根据不同命令名的case,选择不同的解析策略(handlerxxx函数)对FTP请求消息进行解析。
改进2:基于对象的结构改进
C++中,更多时候,处理的代码是类的成员函数,而不是普通函数。
此时,可将前面的结构体数组 转换成一个static vector<> handlers_,然后遍历handleCommand,匹配到命令名就调用对应的命令处理函数即可。
也许,解析FTP消息功能,会包含在像下面FtpSession类中:
class FtpSession
{
public:
using HandlerFuncType = void (FtpSession::*)(const std::string &);
using HandlerItemType = std::pair<const std::string, HandlerFuncType>;
...
void handleCommand(const std::string& ftpText);
private:
void handleUSER(const std::string& arg);
void handlePASS(const std::string& arg);
void handleCWD(const std::string& arg);
...
static std::vector<HandlerItemType> const handlers_; // 注意handlers_是static属性
};
static成员handlers_,是建立FTP命令名到解析命令的策略的映射的关键:
std::vector<FtpSession::HandlerItemType> const FtpSession::handlers_ = {
{ "USER", &FtpSession::handleUSER },
{ "PASS", &FtpSession::handlePASS },
{ "CWD", &FtpSession::handleCWD },
...
}
策略模式本质,是在上下文中,根据不同case,选择不同算法的策略,对协议进行解析:
void FtpSession::handleCommand(const std::string& ftpText)
{
std::string name; // 命令名
std::string arg; // 命令参数
[name, arg] = str_split(ftpText, ' ');
auto item = std::find_if(handlers_.begin(), handlers_.end(), [&name](const HandlerItemType& e) {
return name == e.first;
});
if (item == handlers_.end()) { // fail
handlerUnknownCommand();
}
else {
auto const hanlder = item->second;
(this->*handler)(arg); // Callback command handler // 注意这里的this->*handler
}
}
为什么要用this->*handler进行回调?
因为handlers_.second存放的是void (FtpSession::)(const std::string &),也就是FtpSession成员函数地址,这是一个指针,this->handler表示对当前this对象的成员函数。
通过这种方式,巧妙地将对象的成员通过static成员,转发给当前对象的其他函数。