标签:
有人问我,提高编程水平最有效的办法是什么?我想了很久,终于发现最有效的办法,其实是反反复复地修改和推敲代码。如果你总是匆匆写出代码,却从来不回头去推敲,修改和提炼,其实是不可能提高编程水平的。你会制造出越来越多平庸甚至糟糕的代码。
有位文豪说得好:“看一个作家的水平,不是看他发表了多少文字,而要看他的废纸篓里扔掉了多少。” 我觉得同样的理论适用于编程。好的程序员,他们删掉的代码,比留下来的还要多很多。如果你看见一个人写了很多代码,却没有删掉多少,那他的代码一定有很多垃圾。
人们都讨厌“面条代码”(spaghetti code),因为它就像面条一样绕来绕去,没法理清头绪。那么优雅的代码一般是什么形状的呢?经过多年的观察,我发现优雅的代码,在形状上有一些明显的特征。
如果我们忽略具体的内容,从大体结构上来看,优雅的代码看起来就像是一些整整齐齐,套在一起的盒子。如果跟整理房间做一个类比,就很容易理解。如果你把所有物品都丢在一个很大的抽屉里,那么它们就会全都混在一起。你就很难整理,很难迅速的找到需要的东西。但是如果你在抽屉里再放几个小盒子,把物品分门别类放进去,那么它们就不会到处乱跑,你就可以比较容易的找到和管理它们。
优雅的代码的另一个特征是,它的逻辑大体上看起来,是枝丫分明的树状结构(tree)。这是因为程序所做的几乎一切事情,都是信息的传递和分支。你可以把代码看成是一个电路,电流经过导线,分流或者汇合。如果你是这样思考的,你的代码里就会比较少出现只有一个分支的if语句。
真正的模块化,并不是文本意义上的,而是逻辑意义上的。一个模块应该像一个电路芯片,它有定义良好的输入和输出。
想要达到很好的模块化,你需要做到以下几点:
有些人以为写很多注释就可以让代码更加可读,然而却发现事与愿违。注释不但没能让代码变得可读,反而由于大量的注释充斥在代码中间,让程序变得障眼难读。而且代码的逻辑一旦修改,就会有很多的注释变得过时,需要更新。修改注释是相当大的负担,所以大量的注释,反而成为了妨碍改进代码的绊脚石==但我认为模型等数据的注释是必须的。
实际上,真正优雅可读的代码,是几乎不需要注释的。如果你发现需要写很多注释,那么你的代码肯定是含混晦涩,逻辑不清晰的。
有少数的时候,你也许会为了绕过其他一些代码的设计问题,采用一些违反直觉的作法。这时候你可以使用很短注释,说明为什么要写成那奇怪的样子。这样的情况应该少出现,否则这意味着整个代码的设计都有问题。
如果没能合理利用程序语言提供的优势,你会发现程序还是很难懂,以至于需要写注释。所以我现在告诉你一些要点,也许可以帮助你大大减少写注释的必要:
String msg;
if(…){
msg = “succeed”;
log.info(msg);
} else {
msg = “failed”;
log.info(msg);
}
程序语言都喜欢标新立异,提供这样那样的“特性”,然而有些特性其实并不是什么好东西。很多特性都经不起时间的考验,最后带来的麻烦,比解决的问题还多。很多人盲目的追求“短小”和“精悍”,或者为了显示自己头脑聪明,学得快,所以喜欢利用语言里的一些特殊构造,写出过于“聪明”,难以理解的代码。
并不是语言提供什么,你就一定要把它用上的。实际上你只需要其中很小的一部分功能,就能写出优秀的代码。我一向反对“充分利用”程序语言里的所有特性。实际上,我心目中有一套最好的构造。不管语言提供了多么“神奇”的,“新”的特性,我基本都只用经过千锤百炼,我觉得值得信赖的那一套。
现在针对一些有问题的语言特性,我介绍一些我自己使用的代码规范,并且讲解一下为什么它们能让代码更简单。
1)、避免使用自增减表达式。自增减操作表达式其实是历史遗留的设计失误。它们含义蹊跷,非常容易弄错。它们把读和写这两种完全不同的操作,混淆缠绕在一起,把语义搞得乌七八糟。含有它们的表达式,结果可能取决于求值顺序,所以它可能在某种编译器下能正确运行,换一个编译器就出现离奇的错误。
有人也许以为i++或者++i的效率比拆开之后要高,这只是一种错觉。这些代码经过基本的编译器优化之后,生成的机器代码是完全没有区别的。自增减表达式只有在两种情况下才可以安全的使用。一种是在for循环的update部分,比如for(int i = 0; i < 5; i++)。另一种情况是写成单独的一行,比如i++;。
2)、永远不要省略花括号。
3)、合理使用括号,不要盲目依赖操作符优先级。
4)、避免使用continue和break。循环语句(for,while)里面出现return是没问题的,然而如果你使用了continue或者break,就会让循环的逻辑和终止条件变得复杂,难以确保正确。出现continue或者break的原因,往往是对循环的逻辑没有想清楚。如果你考虑周全了,应该是几乎不需要continue或者break的。如果你的循环里出现了continue或者break,你就应该考虑改写这个循环。改写循环的办法有多种:如果出现了continue,你往往只需要把continue的条件反向,就可以消除continue;如果出现了break,你往往可以把break的条件,合并到循环头部的终止条件里,从而去掉break;有时候你可以把break替换成return,从而去掉break;如果以上都失败了,你也许可以把循环里面复杂的部分提取出来,做成函数调用,之后continue或者break就可以去掉了。
我写代码有一条重要的原则:如果有更加直接,更加清晰的写法,就选择它,即使它看起来更长,更笨,也一样选择它。
每个if语句都有两个分支的理由是:如果if的条件成立,你做某件事情;但是如果if的条件不成立,你应该知道要做什么另外的事情。不管你的if有没有else,你终究是逃不掉,必须得思考这个问题的。嵌套的if语句省略了一些else,依靠语句的“控制流”来处理else的情况,是很难正确的分析和推理的。
由于疏忽而漏掉的分支,全都会自动“掉下去”,最后返回意想不到的结果。即使你看一遍之后确信是正确的,每次读这段代码,你都不能确信它照顾了所有的情况,又得重新推理一遍。这简洁的写法,带来的是反复的,沉重的头脑开销。这就是所谓“面条代码”,因为程序的逻辑分支,不是像一棵枝叶分明的树,而是像面条一样绕来绕去。
使用有两个分支的if语句,只是我的代码可以达到无懈可击的其中一个原因。这样写if语句的思路,其实包含了使代码可靠的一种通用思想:穷举所有的情况,不漏掉任何一个。
程序的绝大部分功能,是进行信息处理。从一堆纷繁复杂,模棱两可的信息中,排除掉绝大部分“干扰信息”,找到自己需要的那一个。正确地对所有的“可能性”进行推理,就是写出无懈可击代码的核心思想。这一节我来讲一讲,如何把这种思想用在错误处理上,因为错误也是“所有可能性的一种”,所以对于返回值都要处理,java中异常也是可能性的一种,所以可能出现的异常也都要处理。
如果你把异常catch了,忽略掉,那么你就不知道foo其实失败了。这就像开车时看到路口写着“前方施工,道路关闭”,还继续往前开。这当然迟早会出问题,因为你根本不知道自己在干什么。
catch异常的时候,你不应该使用Exception这么宽泛的类型。你应该正好catch可能发生的那种异常A。使用宽泛的异常类型有很大的问题,因为它会不经意的catch住另外的异常(比如B)。你的代码逻辑是基于判断A是否出现,可你却catch所有的异常(Exception类),所以当其它的异常B出现的时候,你的代码就会出现莫名其妙的问题,因为你以为A出现了,而其实它没有。这种bug,有时候甚至使用debugger都难以发现。
主要针对java,未整理。
1)、过度工程即将出现的一个重要信号,就是当你过度的思考“将来”,考虑一些还没有发生的事情,还没有出现的需求。比如,“如果我们将来有了上百万行代码,有了几千号人,这样的工具就支持不了了”,“将来我可能需要这个功能,所以我现在就把代码写来放在那里”,“将来很多人要扩充这片代码,所以现在我们就让它变得可重用”……
这就是为什么很多软件项目如此复杂。实际上没做多少事情,却为了所谓的“将来”,加入了很多不必要的复杂性。眼前的问题还没解决呢,就被“将来”给拖垮了。人们都不喜欢目光短浅的人,然而在现实的工程中,有时候你就是得看近一点,把手头的问题先搞定了,再谈以后扩展的问题。
2)、另外一种过度工程的来源,是过度的关心“代码重用”。很多人“可用”的代码还没写出来呢,就在关心“重用”。为了让代码可以重用,最后被自己搞出来的各种框架捆住手脚,最后连可用的代码就没写好。如果可用的代码都写不好,又何谈重用呢?很多一开头就考虑太多重用的工程,到后来被人完全抛弃,没人用了,因为别人发现这些代码太难懂了,自己从头开始写一个,反而省好多事。
3)、过度地关心“测试”,也会引起过度工程。有些人为了测试,把本来很简单的代码改成“方便测试”的形式,结果引入很多复杂性,以至于本来一下就能写对的代码,最后复杂不堪,出现很多bug。
世界上有两种“没有bug”的代码。一种是“没有明显的bug的代码”,另一种是“明显没有bug的代码”。第一种情况,由于代码复杂不堪,加上很多测试,各种coverage,貌似测试都通过了,所以就认为代码是正确的。第二种情况,由于代码简单直接,就算没写很多测试,你一眼看去就知道它不可能有bug。你喜欢哪一种“没有bug”的代码呢?
根据这些,我总结出来的防止过度工程的原则如下:
个人觉得将来、代码重用、测试都是可以考虑,但是考虑的前提是对解决眼前的问题影响不大!
标签:
原文地址:http://blog.csdn.net/blackwolfsky/article/details/51211812