码迷,mamicode.com
首页 > 其他好文 > 详细

编程的智慧博文摘录

时间:2016-04-22 19:47:47      阅读:234      评论:0      收藏:0      [点我收藏+]

标签:

1、反复推敲代码

有人问我,提高编程水平最有效的办法是什么?我想了很久,终于发现最有效的办法,其实是反反复复地修改和推敲代码。如果你总是匆匆写出代码,却从来不回头去推敲,修改和提炼,其实是不可能提高编程水平的。你会制造出越来越多平庸甚至糟糕的代码。

有位文豪说得好:看一个作家的水平,不是看他发表了多少文字,而要看他的废纸篓里扔掉了多少。我觉得同样的理论适用于编程。好的程序员,他们删掉的代码,比留下来的还要多很多。如果你看见一个人写了很多代码,却没有删掉多少,那他的代码一定有很多垃圾。

2、写优雅的代码

人们都讨厌面条代码spaghetti code),因为它就像面条一样绕来绕去,没法理清头绪。那么优雅的代码一般是什么形状的呢?经过多年的观察,我发现优雅的代码,在形状上有一些明显的特征。

如果我们忽略具体的内容,从大体结构上来看,优雅的代码看起来就像是一些整整齐齐,套在一起的盒子。如果跟整理房间做一个类比,就很容易理解。如果你把所有物品都丢在一个很大的抽屉里,那么它们就会全都混在一起。你就很难整理,很难迅速的找到需要的东西。但是如果你在抽屉里再放几个小盒子,把物品分门别类放进去,那么它们就不会到处乱跑,你就可以比较容易的找到和管理它们。

优雅的代码的另一个特征是,它的逻辑大体上看起来,是枝丫分明的树状结构(tree)。这是因为程序所做的几乎一切事情,都是信息的传递和分支。你可以把代码看成是一个电路,电流经过导线,分流或者汇合。如果你是这样思考的,你的代码里就会比较少出现只有一个分支的if语句

3、写模块化的代码

真正的模块化,并不是文本意义上的,而是逻辑意义上的。一个模块应该像一个电路芯片,它有定义良好的输入和输出。

想要达到很好的模块化,你需要做到以下几点:

  • 避免写太长的函数。如果发现函数太大了,就应该把它拆分成几个更小的。通常我写的函数长度都不超过40行。==比如我认为ios中一个控制器如果需要外部传入很多数据,则控制器内部几乎不需要请求数据,被传进来的数据也是外部控制器的数据模型,或者外部数据模型的成员模型,不能将传入的数据罗列传入,这是很烂的写法;一个控制器如果需要外部传入很少数据,则控制器内部很可能需要请求数据,被传进来的数据最多只是控制一下如何请求数据等,如果既需要传入很多数据又需要请求很多数据,则设计是有问题的
  • 制造小的工具函数。如果你仔细观察代码,就会发现其实里面有很多的重复。这些常用的代码,不管它有多短,提取出去做成函数,都可能是会有好处的。有些帮助函数也许就只有两行,然而它们却能大大简化主要函数里面的逻辑。
    有些人不喜欢使用小的函数,因为他们想避免函数调用的开销,结果他们写出几百行之大的函数。这是一种过时的观念。现代的编译器都能自动的把小的函数内联(inline)到调用它的地方,所以根本不产生函数调用,也就不会产生任何多余的开销。
  • 每个函数只做一件简单的事情。有些人喜欢制造一些通用的函数,既可以做这个又可以做那个,它的内部依据某些变量和条件,来选择这个函数所要做的事情。比如,你也许写出这样的函数:
    void foo() {
    •   if (getOS().equals("MacOS")) {
  •     a();
  •   } else {
  •     b();
  •   }
  •   c();
  •   if (getOS().equals("MacOS")) {
  •     d();
  •   } else {
  •     e();
  •   }
  • }
    写这个函数的人,根据系统是否为“MacOS”来做不同的事情。你可以看出这个函数里,其实只有c()是两种系统共有的,而其它的a(), b(), d(), e()都属于不同的分支。
    这种复用其实是有害的。如果一个函数可能做两种事情,它们之间共同点少于它们的不同点,那你最好就写两个不同的函数,否则这个函数的逻辑就不会很清晰,容易出现错误。其实,上面这个函数可以改写成两个函数:
    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();
  • }
    然后制造两个函数:
    void fooMacOS() {
  •   preFoo();
  •   d();
  • }

    void fooOther() {
  •   preFoo();
  •   e();
  • }
    样一来,我们既共享了代码,又做到了每个函数只做一件简单的事情。这样的代码,逻辑就更加清晰
  • 避免使用全局变量和类成员(class member)来传递信息,尽量使用局部变量和参数。有些人写代码,经常用类成员来传递信息,就像这样:
     class A {
  •   String x;


  •   void findX() {
  •       ...
  •       x = ...;
  •   }


  •   void foo() {
  •     findX();
  •     ...
  •     print(x);
  •   }
  • }
    首先,他使用findX(),把一个值写入成员x。然后,使用x的值。这样,x就变成了findXprint之间的数据通道。由于x属于class A,这样程序就失去了模块化的结构。由于这两个函数依赖于成员x,它们不再有明确的输入和输出,而是依赖全局的数据。findXfoo不再能够离开class A而存在,而且由于类成员还有可能被其他代码改变,代码变得难以理解,难以确保正确性(避免使用全局变量和类成员(class member)来传递信息的原因)。
    如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个class,而且更加容易理解,不易出错:
     String findX() {
  •     ...
  •     x = ...;
  •     return x;
  • }
  • void foo() {
  •   int x = findX();
  •   print(x);
  • }

4、写可读的代码

有些人以为写很多注释就可以让代码更加可读,然而却发现事与愿违。注释不但没能让代码变得可读,反而由于大量的注释充斥在代码中间,让程序变得障眼难读。而且代码的逻辑一旦修改,就会有很多的注释变得过时,需要更新。修改注释是相当大的负担,所以大量的注释,反而成为了妨碍改进代码的绊脚石==但我认为模型等数据的注释是必须的

实际上,真正优雅可读的代码,是几乎不需要注释的。如果你发现需要写很多注释,那么你的代码肯定是含混晦涩,逻辑不清晰的。

有少数的时候,你也许会为了绕过其他一些代码的设计问题,采用一些违反直觉的作法。这时候你可以使用很短注释,说明为什么要写成那奇怪的样子。这样的情况应该少出现,否则这意味着整个代码的设计都有问题。

如果没能合理利用程序语言提供的优势,你会发现程序还是很难懂,以至于需要写注释。所以我现在告诉你一些要点,也许可以帮助你大大减少写注释的必要:

  1. 使用有意义的函数和变量名字。如果你的函数和变量的名字,能够切实的描述它们的逻辑,那么你就不需要写注释来解释它在干什么。
  2. 局部变量应该尽量接近使用它的地方。如果你看透了局部变量的本质——它们就是电路里的导线,那你就能更好的理解近距离的好处。变量定义离用的地方越近,导线的长度就越短。你不需要摸着一根导线,绕来绕去找很远,就能发现接收它的端口,这样的电路就更容易理解。
  3. 局部变量名字应该简短。,如果超过了三个单词连在一起,其实是很碍眼的东西。所以如果你能用一个单词表示同样的意义,那当然更好。
  4. 不要重用局部变量。很多人写代码不喜欢定义新的局部变量,而喜欢重用同一个局部变量,通过反复对它们进行赋值,来表示完全不同意思。比如这样写:

String msg;

if(…){

msg = “succeed”;

log.info(msg);

} else {

msg = “failed”;

log.info(msg);

}

  1. 虽然这样在逻辑上是没有问题的,然而却不易理解,容易混淆。变量msg两次被赋值,表示完全不同的两个值。它们立即被log.info使用,没有传递到其它地方去。这种赋值的做法,把局部变量的作用域不必要的增大,让人以为它可能在将来改变,也许会在其它地方被使用。更好的做法,其实是定义两个变量。
  2. 把复杂的逻辑提取出去,做成帮助函数。有些人写的函数很长,以至于看不清楚里面的语句在干什么,所以他们误以为需要写注释。如果你仔细观察这些代码,就会发现不清晰的那片代码,往往可以被提取出去,做成一个函数,然后在原来的地方调用。由于函数有一个名字,这样你就可以使用有意义的函数名来代替注释。
  3. 把复杂的表达式提取出去,做成中间变量。如Pizza pizza = makePizza(crust(salt(), butter()),topping(onion(), tomato(), sausage()));可以提取为Crust crust = crust(salt(), butter());Topping topping = topping(onion(), tomato(), sausage());Pizza pizza = makePizza(crust, topping);
  4. 在合理的地方换行。有些人喜欢利用IDE的自动换行机制,编辑之后用一个热键把整个代码重新格式化一遍,IDE就会把超过行宽限制的代码自动折行。可是这种自动这行,往往没有根据代码的逻辑来进行,不能帮助理解代码。为了避免IDE把这些手动调整好的换行弄乱,很多IDE(比如IntelliJ)的自动格式化设定里都有保留原来的换行符的设定。

4、写简单的代码

程序语言都喜欢标新立异,提供这样那样的特性,然而有些特性其实并不是什么好东西。很多特性都经不起时间的考验,最后带来的麻烦,比解决的问题还多。很多人盲目的追求短小精悍,或者为了显示自己头脑聪明,学得快,所以喜欢利用语言里的一些特殊构造,写出过于聪明,难以理解的代码。

并不是语言提供什么,你就一定要把它用上的。实际上你只需要其中很小的一部分功能,就能写出优秀的代码。我一向反对充分利用程序语言里的所有特性。实际上,我心目中有一套最好的构造。不管语言提供了多么神奇的,的特性,我基本都只用经过千锤百炼,我觉得值得信赖的那一套。

现在针对一些有问题的语言特性,我介绍一些我自己使用的代码规范,并且讲解一下为什么它们能让代码更简单。

1)、避免使用自增减表达式。自增减操作表达式其实是历史遗留的设计失误。它们含义蹊跷,非常容易弄错。它们把读和写这两种完全不同的操作,混淆缠绕在一起,把语义搞得乌七八糟。含有它们的表达式,结果可能取决于求值顺序,所以它可能在某种编译器下能正确运行,换一个编译器就出现离奇的错误。

有人也许以为i++或者++i的效率比拆开之后要高,这只是一种错觉。这些代码经过基本的编译器优化之后,生成的机器代码是完全没有区别的。自增减表达式只有在两种情况下才可以安全的使用。一种是在for循环的update部分,比如for(int i = 0; i < 5; i++)。另一种情况是写成单独的一行,比如i++;

2)、永远不要省略花括号。

3)、合理使用括号,不要盲目依赖操作符优先级。

4)、避免使用continuebreak。循环语句(forwhile)里面出现return是没问题的,然而如果你使用了continue或者break,就会让循环的逻辑和终止条件变得复杂,难以确保正确。出现continue或者break的原因,往往是对循环的逻辑没有想清楚。如果你考虑周全了,应该是几乎不需要continue或者break的。如果你的循环里出现了continue或者break,你就应该考虑改写这个循环。改写循环的办法有多种:如果出现了continue,你往往只需要把continue的条件反向,就可以消除continue;如果出现了break,你往往可以把break的条件,合并到循环头部的终止条件里,从而去掉break;有时候你可以把break替换成return,从而去掉break;如果以上都失败了,你也许可以把循环里面复杂的部分提取出来,做成函数调用,之后continue或者break就可以去掉了

5、写直观的代码

我写代码有一条重要的原则:如果有更加直接,更加清晰的写法,就选择它,即使它看起来更长,更笨,也一样选择它。

6、写无懈可击的代码

每个if语句都有两个分支的理由是:如果if的条件成立,你做某件事情;但是如果if的条件不成立,你应该知道要做什么另外的事情。不管你的if有没有else,你终究是逃不掉,必须得思考这个问题的。嵌套的if语句省略了一些else,依靠语句的控制流来处理else的情况,是很难正确的分析和推理的。

由于疏忽而漏掉的分支,全都会自动掉下去,最后返回意想不到的结果。即使你看一遍之后确信是正确的,每次读这段代码,你都不能确信它照顾了所有的情况,又得重新推理一遍。这简洁的写法,带来的是反复的,沉重的头脑开销。这就是所谓面条代码,因为程序的逻辑分支,不是像一棵枝叶分明的树,而是像面条一样绕来绕去。

使用有两个分支的if语句,只是我的代码可以达到无懈可击的其中一个原因。这样写if语句的思路,其实包含了使代码可靠的一种通用思想:穷举所有的情况,不漏掉任何一个。

7、正确处理错误

程序的绝大部分功能,是进行信息处理。从一堆纷繁复杂,模棱两可的信息中,排除掉绝大部分干扰信息,找到自己需要的那一个。正确地对所有的可能性进行推理,就是写出无懈可击代码的核心思想。这一节我来讲一讲,如何把这种思想用在错误处理上,因为错误也是“所有可能性的一种”,所以对于返回值都要处理,java中异常也是可能性的一种,所以可能出现的异常也都要处理。

如果你把异常catch了,忽略掉,那么你就不知道foo其实失败了。这就像开车时看到路口写着前方施工,道路关闭,还继续往前开。这当然迟早会出问题,因为你根本不知道自己在干什么。

catch异常的时候,你不应该使用Exception这么宽泛的类型。你应该正好catch可能发生的那种异常A。使用宽泛的异常类型有很大的问题,因为它会不经意的catch住另外的异常(比如B)。你的代码逻辑是基于判断A是否出现,可你却catch所有的异常(Exception类),所以当其它的异常B出现的时候,你的代码就会出现莫名其妙的问题,因为你以为A出现了,而其实它没有。这种bug,有时候甚至使用debugger都难以发现。

8、正确处理null指针

主要针对java,未整理。

9、防止过度工程

1)、过度工程即将出现的一个重要信号,就是当你过度的思考将来,考虑一些还没有发生的事情,还没有出现的需求。比如,如果我们将来有了上百万行代码,有了几千号人,这样的工具就支持不了了将来我可能需要这个功能,所以我现在就把代码写来放在那里将来很多人要扩充这片代码,所以现在我们就让它变得可重用”……

这就是为什么很多软件项目如此复杂。实际上没做多少事情,却为了所谓的将来,加入了很多不必要的复杂性。眼前的问题还没解决呢,就被将来给拖垮了。人们都不喜欢目光短浅的人,然而在现实的工程中,有时候你就是得看近一点,把手头的问题先搞定了,再谈以后扩展的问题。

2)、另外一种过度工程的来源,是过度的关心代码重用。很多人可用的代码还没写出来呢,就在关心重用。为了让代码可以重用,最后被自己搞出来的各种框架捆住手脚,最后连可用的代码就没写好。如果可用的代码都写不好,又何谈重用呢?很多一开头就考虑太多重用的工程,到后来被人完全抛弃,没人用了,因为别人发现这些代码太难懂了,自己从头开始写一个,反而省好多事。

3)、过度地关心测试,也会引起过度工程。有些人为了测试,把本来很简单的代码改成方便测试的形式,结果引入很多复杂性,以至于本来一下就能写对的代码,最后复杂不堪,出现很多bug

世界上有两种没有bug”的代码。一种是没有明显的bug的代码,另一种是明显没有bug的代码。第一种情况,由于代码复杂不堪,加上很多测试,各种coverage,貌似测试都通过了,所以就认为代码是正确的。第二种情况,由于代码简单直接,就算没写很多测试,你一眼看去就知道它不可能有bug。你喜欢哪一种没有bug”的代码呢?

根据这些,我总结出来的防止过度工程的原则如下:

  1. 先把眼前的问题解决掉,解决好,再考虑将来的扩展问题。
  2. 先写出可用的代码,反复推敲,再考虑是否需要重用的问题。
  3. 先写出可用,简单,明显没有bug的代码,再考虑测试的问题。

个人觉得将来、代码重用、测试都是可以考虑,但是考虑的前提是对解决眼前的问题影响不大

编程的智慧博文摘录

标签:

原文地址:http://blog.csdn.net/blackwolfsky/article/details/51211812

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!