编程的智慧

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章