[转载]重构:代码之丑(二)

[转载]代码之丑(二) – 梦想风暴 – 博客大巴.

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://dreamhead.blogbus.com/logs/81144620.html

这是一个长长的判断条件:
if ( strcmp(rec.type, “PreDropGroupSubs”) == 0
|| strcmp(rec.type, “StopUserGroupSubsCancel”) == 0
|| strcmp(rec.type, “QFStopUserGroupSubs”) == 0
|| strcmp(rec.type, “QFStopUserGroupSubsCancel”) == 0
|| strcmp(rec.type, “QZStopUserGroupSubs”) == 0
|| strcmp(rec.type, “QZStopUserGroupSubsCancel”) == 0
|| strcmp(rec.type, “SQStopUserGroupSubs”) == 0
|| strcmp(rec.type, “SQStopUserGroupSubsCancel”) == 0
|| strcmp(rec.type, “StopUseGroupSubs”) == 0
|| strcmp(rec.type, “PreDropGroupSubsCancel”) == 0)

之所以注意到它,因为最后两个条件是最新修改里面加入的,换句话说,这不是一次写就的代码。单就这一次而言,只改了两行,这是可以接受的。但这是遗留代码。每次可能只改了一两行,通常我们会不只一次踏入这片土地。经年累月,代码成了这个样子。

这并非我接触过的最长的判断条件,这种代码极大的开拓了我的视野。现在的我,即便面对的是一屏无法容纳的条件,也可以坦然面对了,虽然显示器越来越大。

其实,如果这个判断条件是这个函数里仅有的东西,我也就忍了。遗憾的是,大多数情况下,这只不过是一个更大函数中的一小段而已。

为了让这段代码可以接受一些,我们不妨稍做封装:
bool shouldExecute(Record& rec) {
return (strcmp(rec.type, “PreDropGroupSubs”) == 0
|| strcmp(rec.type, “StopUserGroupSubsCancel”) == 0
|| strcmp(rec.type, “QFStopUserGroupSubs”) == 0
|| strcmp(rec.type, “QFStopUserGroupSubsCancel”) == 0
|| strcmp(rec.type, “QZStopUserGroupSubs”) == 0
|| strcmp(rec.type, “QZStopUserGroupSubsCancel”) == 0
|| strcmp(rec.type, “SQStopUserGroupSubs”) == 0
|| strcmp(rec.type, “SQStopUserGroupSubsCancel”) == 0
|| strcmp(rec.type, “StopUseGroupSubs”) == 0
|| strcmp(rec.type, “PreDropGroupSubsCancel”) == 0);
}

if (shouldExecute(rec)) {

}

现在,虽然条件依然还是很多,但和原来庞大的函数相比,至少它已经被控制在一个相对较小的函数里了。更重要的是,通过函数名,我们终于有机会说出这段代码判断的是什么了。

提取函数把这段代码混乱的条件分离开来,它还是可以继续改进的。比如,我们把判断的条件进一步提取:
bool shouldExecute(Record& rec) {
static const char* execute_type[] = {
“PreDropGroupSubs”,
“StopUserGroupSubsCancel”,
“QFStopUserGroupSubs”,
“QFStopUserGroupSubsCancel”,
“QZStopUserGroupSubs”,
“QZStopUserGroupSubsCancel”,
“SQStopUserGroupSubs”,
“SQStopUserGroupSubsCancel”,
“StopUseGroupSubs”,
“PreDropGroupSubsCancel”
};

int size = ARRAY_SIZE(execute_type);
for (int i = 0; i < size; i++) {
if (strcmp(rec.type, execute_type[i]) == 0) {
return true;
}
}

return false;
}

这样的话,再加一个新的type,只要在数组中增加一个新的元素即可。如果我们有兴趣的话,还可以进一步对这段代码进行封装,把这个type列表变成声明式,进一步提高代码的可读性。

发现这种代码很容易,只要看到在长长的判断条件,就是它了。要限制这种代码的存在,我们只要以设定一个简单的规则:

  • 判断条件里面不允许多个条件的组合

在实际的应用中,我们会把“3”定义为“多”,也就是如果有两个条件的组合,可以接受,如果是三个,还是改吧!

虽然通过不断调整,这段代码已经不同于之前,但它依然不是我们心目中的理想代码。出现这种代码,往往意味背后有更严重的设计问题。不过,它并不是这里讨论的内容,这里的讨论就到此为止吧!

sinojelly在《代码之丑(二) 》的评论里问了个问题,“把这个type列表变成声明式”,什么样的声明式?

好吧!我承认,我偷懒了,为了省事,一笔带过了。简单理解声明式的风格,就是把描述做什么,而不是怎么做。一个声明式编程的例子是Rails里面的数据关联,为人熟知的has_many和belongs_to。通过声明,模型类就会具备一些数据关联的能力。

具体到实际开发里,声明式编程需要有两个部分:一方面是一些基础的框架性代码,另一方面是应用层面如何使用。框架代码通常来说,都不像应用层面代码那么好理解,但有了这个基础,应用代码就会变得简单许多。

针对之前的那段代码,按照声明性编程风格,我改造了代码,下面是框架部分的代码:

#define BEGIN_STR_PREDICATE(predicate_name) \
bool predicate_name(const char* field) { \
static const char* predicate_true_fields[] = {

#define STR_PREDICATE_ITEM(item) #item ,

#define END_STR_PREDICATE \
};\
\
int size = ARRAY_SIZE(predicate_true_fields);\
for (int i = 0; i < size; i++) { \
if (strcmp(field, predicate_true_fields[i]) == 0) {\
return true;\
}\
}\
\
return false;\
}

这里用到了C/C++常见的宏技巧,为的就是让应用层面的代码写起来更像声明。对比一下之前的函数,就会发现,实际上二者几乎是一样的。有了框架,就该应用了:

BEGIN_STR_PREDICATE(shouldExecute)
STR_PREDICATE_ITEM(PreDropGroupSubs)
STR_PREDICATE_ITEM(StopUserGroupSubsCancel)
STR_PREDICATE_ITEM(QFStopUserGroupSubs)
STR_PREDICATE_ITEM(QFStopUserGroupSubsCancel)
STR_PREDICATE_ITEM(QZStopUserGroupSubs)
STR_PREDICATE_ITEM(QZStopUserGroupSubsCancel)
STR_PREDICATE_ITEM(SQStopUserGroupSubs)
STR_PREDICATE_ITEM(SQStopUserGroupSubsCancel)
STR_PREDICATE_ITEM(StopUseGroupSubs)
STR_PREDICATE_ITEM(SQStopUserGroupSubsCancel)
STR_PREDICATE_ITEM(StopUseGroupSubs)
STR_PREDICATE_ITEM(PreDropGroupSubsCancel)
END_STR_PREDICATE

shouldExecute就此重现出来了。不过,这段代码已经不再像一个函数,而更像一段声明,这就是我们的目标。有了这个基础,实现一个新的函数,不过是做一段新的声明而已。

接下来就是如何使用了,与之前略有差异的是,这里为了更好的通用性,把字符串作为参数传了进去,而不是原来的整个类对象。
shouldExecute(r.type);

虽然应用代码变得简单了,但写出框架的结构是需要一定基础的。它不像应用代码那样来得平铺直叙,但其实也没那么难,只不过很多人从没有考虑把代码写成这样。只要换个角度去思考,多多练习,也就可以驾轻就熟了。

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏