BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

专栏:代码之丑(二)——长长的条件

| 作者 郑晔 关注 2 他的粉丝 发布于 2010年11月24日. 估计阅读时间: 11 分钟 | CNUTCon 了解国内外一线大厂50+智能运维最新实践案例。

这是一个长长的判断条件:

if (strcmp(type, “DropGroup") == 0
    || strcmp(type, "CancelUserGroup") == 0
    || strcmp(type, "QFUserGroup") == 0
    || strcmp(type, "CancelQFUserGroup") == 0
    || strcmp(type, "QZUserGroup") == 0
    || strcmp(type, "CancelQZUserGroup") == 0
    || strcmp(type, "SQUserGroup") == 0
    || strcmp(type, "CancelSQUserGroup") == 0
    || strcmp(type, “UseGroup") == 0
    || strcmp(type, "CancelGroup") == 0)

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

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

其实,如果这个判断条件是这个函数里仅有的东西,我也是可以接受的。遗憾的是,大多数情况下,这只不过是一个更大函数中的一小段而已。为了让这段代码可以接受一些,我们不妨稍做封装:

  bool shouldExecute(const char* type) {
    return (strcmp(type, “DropGroup") == 0
    || strcmp(type, "CancelUserGroup") == 0
    || strcmp(type, "QFUserGroup") == 0
    || strcmp(type, "CancelQFUserGroup") == 0
    || strcmp(type, "QZUserGroup") == 0
    || strcmp(type, "CancelQZUserGroup") == 0
    || strcmp(type, "SQUserGroup") == 0
    || strcmp(type, "CancelSQUserGroup") == 0
    || strcmp(type, “UseGroup") == 0
    || strcmp(type, "CancelGroup") == 0);
  }
  if (shouldExecute(type)) {
    ...
  }

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

虽然提取函数把这段代码混乱的条件分离开来,它还是可以继续改进的。比如,我们把判断的条件进一步提取:

bool shouldExecute(const char* type) {
  static const char* execute_type[] = {
    "DropGroup",
    "CancelUserGroup",
    "QFUserGroup",
    "CancelQFUserGroup",
    "QZUserGroup",
    "CancelQZUserGroup",
    "SQUserGroup",
    "CancelSQUserGroup",
    "UseGroup",
    "CancelGroup"
  };
  int size = ARRAY_SIZE(execute_type);
  for (int i = 0; i < size; i++) {
    if (strcmp(type, execute_type[i]) == 0) {
      return true;
    }
  }
  return false;
}

这样的话,如果以后要加一个新的type,只要在数组中增加一个新的元素即可。这段代码还可以进一步封装,把这个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(DropGroup )
  STR_PREDICATE_ITEM(CancelUserGroup )
  STR_PREDICATE_ITEM(QFUserGroup )
  STR_PREDICATE_ITEM(CancelQFUserGroup )
  STR_PREDICATE_ITEM(QZUserGroup )
  STR_PREDICATE_ITEM(CancelQZUserGroup )
  STR_PREDICATE_ITEM(SQUserGroup )
  STR_PREDICATE_ITEM(CancelSQUserGroup )
  STR_PREDICATE_ITEM(UseGroup )
  STR_PREDICATE_ITEM(CancelGroup )
END_STR_PREDICATE 

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

使用这个新函数的方法依然如故:

if (shouldExecute(type)) {
    ...
  }

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

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

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

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

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

作者简介:

郑晔,ThoughtWorks公司咨询师,拥有多年企业级软件开发经验,热衷于探索各种程序设计语言在真实软件开发中所能发挥的威力,致力于探寻合理的软件开发方式,加入ThoughtWorks公司后,投入到敏捷开发方法的实践之中,为其他公司提供敏捷开发方法方面的咨询服务。他的blog是梦想风暴

查看原文:代码之丑(二)代码之丑(二)(续)

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

看过两期这个文章,感觉和Flower 那本著名的重构书籍大同小异 by luo aaron

看过两期这个文章,感觉和Flower 那本著名的重构书籍大同小异

Re: 看过两期这个文章,感觉和Flower 那本著名的重构书籍大同小异 by 万 安平

BJ 我怎么感觉像 那本代码大全呢?

换成声明式的宏好在哪里呢? by Kraft Bai

code 减少?
操作 减少?
逻辑 减少?
更容易懂?

我觉得宏除了在一些明显胜出函数的地方需要使用,其他地方少用为妙。C语言还是以C风格处理比较好。

教条 by 毕 成栋

如果type不是变化点,我觉得原来的代码挺好。
重构代码的原因是封装变化、使代码美观简单。
我觉得最起码应该交代个背景。如果没有前因后果的,要这样改那样改是不是太武断了?
举个例子,Unix的艺术代码,如果拿到商务系统,那就是噩梦!

Re: 教条 by 拾 叁

赞同的。这种只为一个小小的判断令到与读者跳来跳去的代码,基本上可以去死了。还是顺序的舒服。

Re: 教条 by 毕 成栋

是的

Re: 换成声明式的宏好在哪里呢? by wei dai

作者已经点明了在框架代码里用。你可以看看MFC之类的框架,再琢磨一下它们为什么会这么写。

Re: 换成声明式的宏好在哪里呢? by 毕 成栋

作者没有点明变化点在哪里?为什么要重构它?
框架代码也是给人看的,在牺牲易读性的同时,必须带来一些易维护性的好处。
相比Flower的重构,代码变更的来龙去脉,作者交代的似乎太简单了点。

Re: 教条 by Peng Shawn

事实上我赞同作者第一个版本的shouldExecute,至于第二版本不是很认同,它过于使用技巧,让代码逻辑变复杂了。
我也赞同拾叁说得有道理。
但在一段短小的代码里,我们基本上可以顺序写下去,可当代码变长时调试变成一件困难的事情,因为代码行数太多了,你每按一次调试快捷键,代码会在下一行被高亮,这时你都必须得去检查某些变量值是否正确,这个检查变量值正确是个复杂的过程,你要去判断走完每一行代码将会期望得到什么值,当不是你的期望时你会停下检查代码修改BUG,而尽量提取方法,将可以使原方法变短,这时你做的是重复更少次数的类似检查,当你的方法不是你预期的那样时,你将可直接针对这个更短小的方法写新的单元测试,而不是又从原方法开始,我不敢说这样会更有效率,你可尝试一下。

Re: 教条 by 毕 成栋

只能说例子不好,本身strcmp(type, “DropGroup") == 0这一句话没有必要做单元测试。||操作符也没有必要做单元测试

学会抽象和总结就不会出现这样的问题 by Yi Bo

知道这个是判断是否在一个数组之中,有的语言有内置的函数可用的

简单地说,首先将【做什么】与【怎么做】相隔离,然后是消除重复代码! by 高 翌翔

将【做什么】与【怎么做】相隔离,专业术语就是抽象和封装,
当我们从不同的层次上看待问题域时,抽象和封装的结果往往大相径庭。

层次可分为:代码、类、组件、应用系统。

本文的层次是代码,抽象和封装的方法通常是方法,而且作者已经完成了;
接下来就是消除重复代码,作者提供了几种方法,这就比较灵活了的。

Re: 换成声明式的宏好在哪里呢? by Kraft Bai

作者已经点明了在框架代码里用。你可以看看MFC之类的框架,再琢磨一下它们为什么会这么写。


从这段code跳跃到框架,有点远。作者说的原因是这个
"这里用到了C/C++常见的宏技巧,为的就是让应用层面的代码写起来更像声明"

框架是框架,有他该用的地方,也有不该用的地方。框架首先是设计,然后再考虑code写法的问题,而不是反过来

说实话 这样写还不如写个配置文件,然后用个脚本build时展开成C code再编译

bool shouldExecute(const char* type) {
static const char* execute_type[] = {
#include "namearray.h"
};
int size = ARRAY_SIZE(execute_type);
for (int i = 0; i < size; i++) {
if (strcmp(type, execute_type) == 0) {
return true;
}
}
return false;
}

namearray.h 可以直接写,或者用script生成
比如将其中的"号去掉,再用脚本处理下

重构并不限于只用原本的语言,同时也不要拙劣的模仿别的语言。

到封装函数足矣 by Shichao Liu

首先宏我是显然不喜欢的, 人员流动这么多, 时间又紧, 非必要的宏能不用就不用, 浪费新手的时间。

搞个数组,遍历一下,也有风险, 如果哪天逻辑变了, 如果需要超越ctrcpy==0的判断呢?

或者根本的问题在更框架的层面, 为啥会对那么多固定的字符串需要筛选比较呢? 是不是应该用点多态, state模式什么的来解决呢?

代码少不代表代码优美 by Wong Peter

第3、4两段代码都是丑陋的代表。

优美的代码应该是统一的;应该是容易(快速)理解的。

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

15 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT