编程的聪慧
最近读到了一篇很好的关于编程思考的文章,思考之后整理一下,尤其是里面的一些代码片段,很有代表性,希望以后回望时仍然有收获。
原文地址:http://kb.cnblogs.com/page/549080/
编程是创造性的工作,需要灵感和汗水,需要不断思考和实践,同时还需要有人指点迷津,以使自己以最优的方式成长,兼具速度和质量。
反复推敲代码
提高编程水平最有效的办法是什么?是反反复复地修改和推敲代码。
有位文豪说得好:“看一个作家的水平,不是看他发表了多少文字,而要看他的废纸篓里扔掉了多少。”同样的理论适用于编程。好的程序员,他们删掉的代码,比留下来的还要多很多。去糟粕,留精华,这是普遍规律。
写优雅的代码
优雅的代码整整齐齐,逻辑清晰,无论是功能分类,还是流程细节,都让人觉得从容,优雅。
程序所做的几乎一切事情,都是信息的传递和分支。类比电路,电流经过导线,分流或者汇合。如果你是这样思考的,你的代码里就会比较少出现只有一个分支的 if 语句,它看起来就会像这个样子:
if (...) { if (...) { ... } else { ... } } else if (...) { ... } else { ... }
注意到了吗?在上面的代码里面,if 语句几乎总是有两个分支。它们有可能嵌套,有多层的缩进,而且 else 分支里面有可能出现少量重复的代码。然而这样的结构,逻辑却非常严密和清晰。写模块化的代码
模块化的代码,不是简单将功能文件放入不同文件和目录,也不是强行将不同功能分成不同函数。一个模块应该像一个电路芯片,有明确的输入和输出。实际上一种很好的模块化方法早已经存在,它的名字叫做“函数”。每一个函数都有明确的输入(参数)和输出(返回值),同一个文件里可以包含多个函数,所以其实根本不需要把代码分开在多个文件或者目录里面,同样可以完成代码的模块化。甚至把代码全都写在同一个文件里,却仍然是非常模块化的代码。
想要做到代码模块化,以下几点很关键:
1.避免函数太长。
40-50行即可,一页屏幕或人眼观察能力基本就是4、50行,过长的代码不仅不易读而且容易造成逻辑混乱。
2.制作小的工具函数。
一些常用的功能会在代码中反复使用(如输出信息到UI、时间统计等等),提炼成小的工具函数有利于效率和逻辑性的提升。
3.每个函数只做一件简单的事。
有些人喜欢制造一些“通用”的函数,既可以做这个又可以做那个,它的内部依据某些变量和条件,来“选择”这个函数所要做的事情。比如,你也许写出这样的函数(注意,很多人愿意这样写):
void foo () { if (getOS () .equals ("MacOS")) { a (); } else { b (); } c (); if (getOS () .equals ("MacOS")) { d (); } else { e (); } }
<span style="font-weight: normal; font-size: 14px; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">写这个函数的人,根据系统是否为“MacOS”来做不同的事情。你可以看出这个函数里,其实只有c()是两种系统共有的,而其它的a(), b(), d(),e()都属于不同的分支。</span>这种“复用”其实是有害的。如果一个函数可能做两种事情,它们之间共同点少于它们的不同点,那你最好就写两个不同的函数,否则这个函数的逻辑就不会很清晰,容易出现错误。其实,上面这个函数可以改写成两个函数:
void fooMacOS () { a (); c (); d (); }
和
void fooOther () { b (); c (); e (); }
如果你发现两件事情大部分内容相同,只有少数不同,多半时候你可以把相同的部分提取出去,做成一个辅助函数。比如,如果你有个函数是这样:
void foo () { a (); b () c (); if (getOS () .equals ("MacOS")) { d (); } else { e (); } }
其中a(),b(),c()都是一样的,只有d()和e()根据系统有所不同。那么你可以把a(),b(),c()提取出去:
void preFoo () { a (); b () c (); }
然后制造两个函数:<span style="font-size: 14px; font-weight: normal; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"></span><pre name="code" class="cpp">void fooMacOS () { preFoo (); d (); }
<span style="font-size: 14px; font-weight: normal; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">和</span><span style="font-size: 14px; font-weight: normal; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"></span><pre name="code" class="cpp">void fooOther () { preFoo (); e (); }
<span style="font-weight: normal; font-size: 14px; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">这样一来,我们既共享了代码,又做到了每个函数只做一件简单的事情。这样的代码,逻辑就更加清晰。</span>
4.避免使用全局变量和类成员(class member)来传递信息,尽量使用局部变量和参数。有些人写代码,经常用类成员来传递信息,就像这样(本人之前一直这么用
):
class A { String x; void findX () { ... x = ...; } void foo () { findX (); ... print (x); } }首先,他使用findX(),把一个值写入成员x。然后,使用x的值。这样,x就变成了findX和print之间的数据通道。由于x属于class A,这样程序就失去了模块化的结构。由于这两个函数依赖于成员x,它们不再有明确的输入和输出,而是依赖全局的数据。findX和foo不再能够离开class A而存在,而且由于类成员还有可能被其他代码改变,代码变得难以理解,难以确保正确性。
如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个 class,而且更加容易理解,不易出错:
String findX () { ... x = ...; return x; } void foo () { int x = findX (); print (x); }
写可读的代码
说到可读的代码,很多人第一反应就是注释,有很多编程规范要求注释量要达到代码总量的30%甚至更高,当然这个问题众说纷纭,但个人认为,良好的代码风格比添加注释更能说明一段代码的功能和含义,过多的注释不仅破坏代码完整性,而且一旦代码修改,很多注释会失效,注释的添加和修改成为很多程序员不愿触碰之殇。
注释常用在以下典型位置:1.说明主要流程时
2.在违反常规思维的设计时
3.在值得留意或预留功能时
同时,以下方法可以帮助你减少注释量的同时维持程序可读性:
1.使用有意义的函数和变量名字。
如果你的函数和变量的名字,能够切实的描述它们的逻辑,那么你就不需要写注释来解释。比如:
// put elephant1 into fridge2 put (elephant1, fridge2);
2.局部变量应该尽量接近使用它的地方。有些人喜欢在函数最开头定义很多局部变量,然后在下面很远的地方使用它,其实可以挪到接近使用它的地方:就像这个样子:
void foo () { ... ... int index = ...; bar (index); ... }
这样读者看到bar (index),不需要向上看很远就能发现index是如何算出来的。而且这种短距离,可以加强读者对于这里的“计算顺序”的理解。否则如果 index 在顶上,读者可能会怀疑,它其实保存了某种会变化的数据,或者它后来又被修改过。如果 index 放在下面,读者就清楚的知道,index 并不是保存了什么可变的值,而且它算出来之后就没变过。
如果你看透了局部变量的本质——它们就是电路里的导线,那你就能更好的理解近距离的好处。变量定义离用的地方越近,导线的长度就越短。你不需要摸着一根导线,绕来绕去找很远,就能发现接收它的端口,这样的电路就更容易理解。
3.局部变量名字应该简短。
这貌似跟第一点相冲突,简短的变量名怎么可能有意义呢?注意我这里说的是局部变量,因为它们处于局部,再加上第 2 点已经把它放到离使用位置尽量近的地方,所以根据上下文你就会容易知道它的意思:
boolean success = deleteFile ("foo.txt"); if (success) { ... } else { ... }
4.不要重用局部变量。以下是一个重用的反例:
String msg; if (...) { msg = "succeed"; log.info (msg); } else { msg = "failed"; log.info (msg); }
从读者心里来讲,看见msg被多次赋值,会思考msg有没有在其他地方赋值,这里用它准备吗等等之类的怀疑。简单改成这样会好得多:
if (...) { String msg = "succeed"; log.info (msg); } else { String msg = "failed"; log.info (msg); }
5.把复杂的逻辑提取出去,做成“帮助函数”。有些人写的函数很长,以至于看不清楚里面的语句在干什么,所以他们误以为需要写注释。如果你仔细观察这些代码,就会发现不清晰的那片代码,往往可以被提取出去,做成一个函数,然后在原来的地方调用。由于函数有一个名字,这样你就可以使用有意义的函数名来代替注释。举一个例子:
... // put elephant1 into fridge2 openDoor (fridge2); if (elephant1.alive ()) { ... } else { ... } closeDoor (fridge2); ...
如果你把这片代码提出去定义成一个函数:
void put (Elephant elephant, Fridge fridge) { openDoor (fridge); if (elephant.alive ()) { ... } else { ... } closeDoor (fridge); }
这样原来的代码就可以改成:
... put (elephant1, fridge2); ...
更加清晰,注释也没必要了。
6.把复杂的表达式提取出去,做成中间变量。
Crust crust = crust (salt (), butter ()); Topping topping = topping (onion (), tomato (), sausage ()); Pizza pizza = makePizza (crust, topping);
7.在合理的地方换行。
if (someLongCondition1() && someLongCondition2() && someLongCondition3() && someLongCondition4()) { ... }
写简单的代码
简单并不代表省略,以下几条建议会帮助你避免因为追求简单而犯错:
1.永远不要省略花括号{}
2.合理使用括号(),不盲目依赖操作符优先级
3.避免使用continue和break
第3条很多人会有疑问,我也思考了一阵,个人认为这是个仁者见仁智者见智的问题,从原文的角度考虑,continue和break是破坏程序顺序执行的额外加入的强逻辑手段,可以考虑这样改写:
1)如果出现了 continue,你往往只需要把 continue 的条件反向,就可以消除 continue。
2)如果出现了 break,你往往可以把 break 的条件,合并到循环头部的终止条件里,从而去掉 break。(这个有点牵强)
3)有时候你可以把 break 替换成 return,从而去掉 break。
4)如果以上都失败了,你也许可以把循环里面复杂的部分提取出来,做成函数调用,之后 continue 或者 break 就可以去掉了。对应的举例我就省略了,有兴趣的可以看原文。
文中还提及如何处理错误、如何处理NULL指针等等,后续我另写文章来总结。