BT

您是否属于早期采用者或者创新人士?InfoQ正在努力为您设计更多新功能。了解更多

专栏:代码之丑(三)——switch陷阱

| 作者 郑晔 发布于 2010年11月25日. 估计阅读时间: 不到一分钟 | 智能化运维、Serverless、DevOps......2017年有哪些最新运维技术趋势?CNUTCon即将为你揭秘!

又见switch:

switch(firstChar) {
  case ‘N’:
    nextFirstChar = ‘O’;
    break;
  case ‘O’:
    nextFirstChar = ‘P’;
    break;
  case ‘P’:
    nextFirstChar = ‘Q’;
    break;
  case ‘Q’:
    nextFirstChar = ‘R’;
    break;
  case ‘R’:
    nextFirstChar = ‘S’;
    break;
  case ‘S’:
    nextFirstChar = ‘T’;
    break;
  case ‘T’:
    throw new IllegalArgument();
  default:
  }

出于多年编程养成的条件反射,我对于switch总会给予更多的关照。在那本大名鼎鼎《重构》里,Martin Fowler专门把 switch语句单独拿出来作为一种坏味道来讨论。研习面向对象编程之后,看见switch,我就会联想到多态,遗憾的是,这段代码和多态没什么关系。仔细阅读这段代码,我找出了其中的规律,nextFirstChar就是firstChar的下一个字符。于是,我改写了这段代码:

switch(firstChar) {
  case ‘N’:
  case ‘O’:
  case ‘P’:
  case ‘Q’:
  case ‘R’:
    nextFirstChar = firstChar + 1;
    break;
  case ‘T’:
    throw new IllegalArgument();
  default:
  }

现在,至少看起来,这段代码已经比原来短了不少。当然这么做基于一个前提,就是这些字母编码的顺序确确实实是连续的。从理论上说,开始那段代码适用性更强。但在实际开发中,我们碰到字母不连续编码的概率趋近于0。

但这段代码究竟是如何产生的呢?我开始研读上下文,原来这段代码是用当前ID产生下一个ID的,比如当前是N0000,下一个就是N0001。如果数字满了,就改变字母,比如当前ID是R9999,下一个就是T0000。在这里,字母也就相当于一位数字,根据情况进行进位,所以有了这段代码。

代码上的注释告诉我,字母的序列只有从N到T,根据这个提示,我再次改写了这段代码:

if (firstChar >= ‘N’ && firstChar <= ‘S”) {
    nextFirstChar = firstChar + 1;
  } else {
    throw new IllegalArgument();
  }

这里统一处理了字母为T和default的情形,严格说来,这和原有代码并不完全等价。但这是了解了需求后做出的决定,换句话说,原有代码在这里的处理中存在漏洞。

修改这段代码,只是运用了非常简单的编程技巧。遗憾的是,即便如此简单的编程技巧,也不是所有开发人员都驾轻就熟的,很多人更习惯于“平铺直叙”。 这种直白造就了代码中的许多鸿篇巨制。我听过不少“编程是体力活”的抱怨,不过,能把写程序干成体力活,也着实不值得同情。写程序,不动脑子,不体力才怪。

无论何时何地,只要switch出现在眼前,请提高警惕,那里多半有坑。

作者简介:

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

查看原文:代码之丑(三)

评价本文

专业度
风格

您好,朋友!

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

获得来自InfoQ的更多体验。

告诉我们您的想法

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

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

受益匪浅 by 陆 伟

每天上班来看一篇,受益匪浅。期待下一篇

很好的系列 by 张 经纬

很好的系列,成长的过程需要被总结和借鉴,可以整理成培训材料
有些情况看起来弱智,可现实都会存在。昨天看到一段代码,很典型的问题,列出来当素材吧:

if ( bussinessType == 0 ) {
if ( devId == 0 ) {
if ( linkId == 0 ) {
if ( year == 0 ) {
if ( month == 0 ) {
if ( day == 0 ) {
if ( hour == 0 ) {
if ( min == 0 ) {
return 0;
} else if ( min < 0 ) {
return -1;
}
return 1;
} else if ( hour < 0 ) {
return -1;
}
return 1;
} else if ( day < 0 ) {
return -1;
}
return 1;
} else if ( month < 0 ) {
return -1;
}
return 1;
} else if ( year < 0 ) {
return -1;
}
return 1;
} else if ( linkId < 0 ) {
return -1;
}
return 1;
} else if ( devId < 0 ) {
return -1;
}
return 1;
} else if ( bussinessType < 0 ) {
return -1;
}
return 1;

Re: 受益匪浅 by 胡 伟红

下一篇明天发布 :)

... by zhang zhikun

为什么用switch还没有搞明白,别说出来害人拉

个人觉得:也就是《代码大全》的皮毛而已 by 冯 希顺

说点不好听的,也就是《代码大全》的皮毛而已。不过,作者毫无疑问已经研究过《代码大全》了,否则在写这样一篇文章前还没研究过《代码大全》那就太说不过去了。。。目前仅作者仅发表了3片文章,以我的愚昧实在看不出同《代码大全》相比有何特别和创新之处。

我个人觉得文章没有说明“switch陷阱” by 毕 成栋

减少第一次代码的“量”还是20%的功,真正的体力活是需求变更。将switch改成if我没看出任何灵活性。switch真正的危害是出现在需求变更的情况下。用多态将switch封装可以将变化点隔离在一处。但是作者确没有写.......

Re: 我个人觉得文章没有说明“switch陷阱” by 曹 云飞

用多态将switch封装是经典的手法,对于这个例子而言并不合适。针对这个需求,这么改是巧妙的。作者这篇文章的确没有说明“经典的switch陷阱”,但是体现了因地制宜的进行重构的精神,不如换个标题,“因地制宜的改造switch陷阱”。

Re: 个人觉得:也就是《代码大全》的皮毛而已 by 瑜珩 王

很多东西都是老生常谈,很多书其实也都没什么新意,但为什么总是有这样的东西呢?因为不管代码大全多好,重构多棒,总是有很多人不看,不学,不用。所以只能一遍一遍的提醒。

Re: 个人觉得:也就是《代码大全》的皮毛而已 by zhang 3

这段代码真没觉得有什么不好。

抛开需求变更来看代码的好坏,只能从是否易读易懂上来看,我看switch还好懂些,什么背景知识都不必知道就可以了。

而且,从需求变化上看,如果求下一个的方法改得没那么有规律性,而且还有点特例啥的,修改后的精炼代码就出问题了。而改写之前的代码可以遇佛杀佛,随你怎么变,我无非就当个表查。

Re: 我个人觉得文章没有说明“switch陷阱” by 毕 成栋

越改越糟。不知道编码规则如果中间断个号,作者要怎么写...

Re: 我个人觉得文章没有说明“switch陷阱” by 毕 成栋

我指的是需求变更

Re: 我个人觉得文章没有说明“switch陷阱” by zhang 3

这个题目,最简单的改法就是改成查表。

重构应该以容易让接手的人维护需求为第一优先 by M. Jwo

最后一次写了,就是有人喜欢这种无益项目只会展现自己好像很会改程式的技巧,那去参加每年的最短程式大赛就好了啊,用1k代码就能写出多种游戏。
反正作者已经写了十篇,篇篇都是类似的,我想钻研这种没帮助代码的人还大有人在,看那么多赞扬的就知道了,建议还是看一些重构的书对自己比较好。
重构应该以容易让接手的人维护需求为第一优先,不是程式码变短就好。
这作者的问题总是在程式码缩短,在编译器日益精进的年代,这是很要不得的事情。
这个例子如果需求规格上面是说N,O,P,Q,R传回该字的下一个字元,T则丢出错误,当然是要写成他改过的样子,但是看程式需求很明显的是N要传回O、O要传回P这样的需求,当需求要新增一个W要传回Z的时候,程式就乱了。

Re: 重构应该以容易让接手的人维护需求为第一优先 by M. Jwo

我写得不够明确,如果使用者明确的说明要传回下一个字元,就该写成传回下一个字元,如果使用者只是写是N要传回O、O要传回P这样的需求,你就不应该自己帮他想是要传回下一个字元,这样自己是变更需求。

俺更喜欢改成这样儿,欢迎大家拍砖~~~ by 高 翌翔

首先肯定,作者的最终优化代码更精简,不过可读性仍有改进的余地。

if (firstChar >= 'N' && firstChar <= 'S') {
nextFirstChar = firstChar + 1;
} else {
throw new IllegalArgument();
}

俺进一步改写为:

{
...
FirstCharShouldBeInLegalRange(firstChar);

nextFirstChar = GetNextFirstChar(firstChar);
...
}

private void FirstCharShouldBeInLegalRange(char value)
{
if (firstChar < 'N' || firstChar > 'S')
throw new IllegalArgument();
}

private char GetNextFirstChar(char value)
{
return (value + 1);
}

修改后的代码功能不变,但凭空多出来两个私有函数,
或许有人会说,这是多此一举(前半句就免了,实在不雅,哈哈)。

但可读性得到了提升,原有的 if else 被封装起来,取而代之的是两句平铺直叙的函数调用。

这种写法并非俺独创,在单元测试代码中遍地都是,但读起来一目了然,一气呵成,痛快之极!
读者根本不用考虑 switch-case 或是 if-else 等分支逻辑(都被封装起来了),

慢慢地俺喜欢上这种写法,把它应用到开发代码中,发现效果依然显著,代码可读性大大提高!

结论:switch-case 与 if-else 虽然可以相互转换、简化,
   但在不可避免使用的情形下,将它们封装成语义明确的方法(函数)更有意义,
   当然这种方法在 Martin Fowler 的 Refactoring 早就提到了,但想用好还需持续实践和反思!<>

Re: 我个人觉得文章没有说明“switch陷阱” by Peng Shawn

是的,太正确了。变成IF会造成更多奇怪的代码,有时候反而不够switch好看,switch困扰了很多人,包括很多SSE。
如果每一个分支都有一段复杂逻辑,那么switch将变得非常难看。
幸好在我所接触过的很多代码中,case里包裹的逻辑真得很少(只有一两行赋值而已),很多人认为逻辑简单,没有必要使用state,当出现Switch时也就意味着状态实在太多了,抽取State会让人觉得烦琐,大多数人认为这样也工作得很好。坦率地讲,我不欢迎switch但也从来不拒绝。
以经验来看,大凡switch内分支都具备一些共性,一般地讲,所有的分支总是在处理同一批状态,于是我们才把这些状态封装成一个对象,而不同的Case则是不同的state,他们看起来会非常完美,但你仍旧得处理分支,你需要使用工厂来创建状态对象。

写的很好 by 有心猴 有心猴

看了几篇,感觉写的很好,至少问题写的都很好,解决方法却是大家都有不同的方法,我更倾向于把对应的东西存入hashtable中,虽然这个例子中是有规律的,但并不是所有的情况都是有规律的,我一般会找到一种更加通用的方式!

Google CEO施密特说一个问题重复20次,才可能有效果 by 霍 泰稳

谷歌CEO施密特说“当我不停地重复一句话,直到我受不了了,差不多这时候员工就会听进去了。通常这是20次左右。”,所以我说尽管《代码大全》很好,尽管“代码之丑”可能只是其一点皮毛,但是谁敢说《代码大全》在手,问题全无呢?看看我们其他童鞋的讨论就晓得这个专栏的益处了 :)

另外我要重复的就是“楼上的各位兄台,如果方便,请在“首选项”里将自己的姓名更新一下,就像陆伟、张经纬、王瑜珩、高翼翔、霍泰稳童鞋那样。我们的目的是希望为InfoQ的读者营造一个真实、纯净的交流环境;另外真实姓名的读者可以优先参加InfoQ定期举办的如QCon、QClub这样的活动。”

Re: 重构应该以容易让接手的人维护需求为第一优先 by 毕 成栋

十分支持!

很高兴如此多的人关注《代码之丑》专栏 by 张 凯峰

为什么要开设这样一个专栏,因为这样的专栏对有些人或者说大多数人是有意义的,如果对一些已经深刻掌握代码重构技能的人已经不是问题,欢迎移步。

技术领域人外有人,专栏作者的写法也不见得就是最好唯一的选择,欢迎提建设性意见,而不是“只不过如此……”的评论。

也不要信手摘来《重构》一段话,或者国外某个网站的某个链接,不妨把自己认为无可挑剔的自己的代码贴出来,供大家赏析学习。

Re: 很高兴如此多的人关注《代码之丑》专栏 by 毕 成栋

孟子曰:“人之患在好为人师。”我们及infoq都切忌不可以盲指盲啊!
我相信大家拍的砖,出发点都是好的。
我们自己是不可能写出无可挑剔的代码,《重构》和国外一些网站是是我们的标杆。

Re: 很高兴如此多的人关注《代码之丑》专栏 by zhang 3

把这一个系列都看了看,有两个我觉得容易给人误导的倾向,当然这个倾向已经快变成共识了。

简短的不重复的代码就一定强过啰嗦的代码。
声明式代码一定强于命令式代码。

作者不可能举过于复杂的例子,这是文章所限,不能强人所难,但问题是,在简单的场合,这两个原则的确是我认为有问题的,再多的原则,我以为都比不上“易读”这两个字。

有很多时候啰嗦的命令式代码恰恰是易读的,它甚至因为恰当的重复而达到这个目的。

过份在每个地方强调这样的写法,我怀疑会造成代码的阅读困难,交流困难,还可能带来一些不易察觉的bug。

简洁的声明式代码的强项更多的体现在架构设计,面对复杂变化时的场合。在例子中恰当的体现这一点,无疑对写作提出了更高的要求。

Re: 很高兴如此多的人关注《代码之丑》专栏 by zhang 3

这些说法,在当前的潮流下可能显得反动了,哈哈。不如再提个更加反动的观点,有时候先写出易读,但是僵硬的代码,面临变化时再去重构,可能比一开始就考虑各种需求变化来得更好。

原因是:我们思考的层次本来就是如此递进的,不先把一些简单的线索先搞清楚,很难面对后面复杂的变化。

对代码风格,我觉得有一个是最高原则,无可置疑,这就是“易读”。

第二段code 漏了case S by Kraft Bai

另外 为啥叫 nextfirstchar? 什么意思?
secondchar? nextchar?

Re: Google CEO施密特说一个问题重复20次,才可能有效果 by Kraft Bai

同意,即使了解也可以温故知新,增加新视角。另外在这里晒比在博客里晒好多了,大家一起讨论,而不是一言堂。

Re: 第二段code 漏了case S by Kraft Bai

对不起,有仔细看了遍原文,了解nextfirstchar命名了

有点迷惑了 by Luo Derek

我看了改过之后的代码,反而看不懂了,估计是我水平问题

Re: 俺更喜欢改成这样儿,欢迎大家拍砖~~~ by 果 林

这个代码可以

很多语言没有字符类型 by Du Song

所以从可移植性的角度,原文一开始的代码未必非常坏。
如果这个函数小于一屏幕,不改也无伤大雅。

Re: 很高兴如此多的人关注《代码之丑》专栏 by 孙 奇辉

有一个是最高原则,无可置疑,这就是“易读”。
--请问,你怎么定义或理解“易读”?我想“易读”是指容易读懂吧,这跟个人素质有关的。例如,台湾同学对同一篇技术文章,从右至左的看繁体字,比大陆同学从左至右看简体字,算是“易读”吧;常春藤名校的、以UNIX和LISP背景的同学,比国内的WINDOWS和C背景的同学看同一段程序,“易读”的感觉不一样吧?

简短的不重复的代码就一定强过啰嗦的代码。
声明式代码一定强于命令式代码。
--很显然,以上的命题是正确的,除非读者一直以来,是以WINDOWS+C/JAVA等为背景的程序员,没有在UNIX/LINUX下的函数式编程的经历或兴趣。

Re: 很高兴如此多的人关注《代码之丑》专栏 by zhang 3

所以我才说“这个倾向已经快变成共识了。”

这事儿扯起来太长,不扯了。

对于生成ID by Shichao Liu

貌似例子是和生成特定格式的ID有关,对于switch没啥代表性。

对于生成ID的特定格式, 建议建立整数和字符串的对应关系, 类似进制转换(比如254=>FE)。

Re: 重构应该以容易让接手的人维护需求为第一优先 by ma duxing

在理,我懂你&顶你。

允许的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通知我

33 讨论

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


找回密码....

Follow

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

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

Like

内容自由定制

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

Notifications

获取更新

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

BT