编程的智慧

来源:互联网 发布:淘宝大码孕妇装排行 编辑:程序博客网 时间:2024/05/29 13:31

最近读到了一篇很好的关于编程思考的文章,思考之后整理一下,尤其是里面的一些代码片段,很有代表性,希望以后回望时仍然有收获。

原文地址: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 fridge2put (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 fridge2openDoor (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指针等等,后续我另写文章来总结。

1 0
原创粉丝点击