前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >编程的智慧特点

编程的智慧特点

作者头像
用户1289394
发布2018-02-28 10:10:02
8330
发布2018-02-28 10:10:02
举报
文章被收录于专栏:Java学习网Java学习网

  编程是一种创造性的工作,是一门艺术。精通任何一门艺术,都需要很多的练习和领悟,所以这里提出的“智慧”,并不是号称一天瘦十斤的减肥药,它并不能代替你自己的勤奋。然而由于软件行业喜欢标新立异,喜欢把简单的事情搞复杂,我希望这些文字能给迷惑中的人们指出一些正确的方向,让他们少走一些弯路,基本做到一分耕耘一分收获。

反复推敲代码

  既然“天才是百分之一的灵感,百分之九十九的汗水”,那我先来谈谈这汗水的部分吧。有人问我,提高编程水平最有效的办法是什么?我想了很久,终于发现最有效的办法,其实是反反复复地修改和推敲代码。

  在 IU 的时候,由于 Dan Friedman 的严格教导,我们以写出冗长复杂的代码为耻。如果你代码多写了几行,这老顽童就会大笑,说:“当年我解决这个问题,只写了 5 行代码,你回去再想想吧……” 当然,有时候他只是夸张一下,故意刺激你的,其实没有人能只用 5 行代码完成。然而这种提炼代码,减少冗余的习惯,却由此深入了我的骨髓。

  有些人喜欢炫耀自己写了多少多少万行的代码,仿佛代码的数量是衡量编程水平的标准。然而,如果你总是匆匆写出代码,却从来不回头去推敲,修改和提炼,其实是不可能提高编程水平的。你会制造出越来越多平庸甚至糟糕的代码。在这种意义上,很多人所谓的“工作经验”,跟他代码的质量,其实不一定成正比。如果有几十年的工作经验,却从来不回头去提炼和反思自己的代码,那么他也许还不如一个只有一两年经验,却喜欢反复推敲,仔细领悟的人。

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

  就像文学作品一样,代码是不可能一蹴而就的。灵感似乎总是零零星星,陆陆续续到来的。任何人都不可能一笔呵成,就算再厉害的程序员,也需要经过一段时间,才能发现最简单优雅的写法。有时候你反复提炼一段代码,觉得到了顶峰,没法再改进了,可是过了几个月再回头来看,又发现好多可以改进和简化的地方。这跟写文章一模一样,回头看几个月或者几年前写的东西,你总能发现一些改进。

  所以如果反复提炼代码已经不再有进展,那么你可以暂时把它放下。过几个星期或者几个月再回头来看,也许就有焕然一新的灵感。这样反反复复很多次之后,你就积累起了灵感和智慧,从而能够在遇到新问题的时候直接朝正确,或者接近正确的方向前进。

写优雅的代码

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

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

写模块化的代码

  有些人吵着闹着要让程序“模块化”,结果他们的做法是把代码分布到多个文件和目录里面,然后把这些目录或者文件叫做“module”。他们甚至把这些目录分放在不同的 VCS repo 里面。结果这样的作法并没有带来合作的流畅,而是带来了许多的麻烦。这是因为他们其实并不理解什么叫做“模块”,肤浅的把代码切割开来,分放在不同的位置,其实非但不能达到模块化的目的,而且制造了不必要的麻烦。

  真正的模块化,并不是文本意义上的,而是逻辑意义上的。一个模块应该像一个电路芯片,它有定义良好的输入和输出。实际上一种很好的模块化方法早已经存在,它的名字叫做“函数”。每一个函数都有明确的输入(参数)和输出(返回值),同一个文件里可以包含多个函数,所以你其实根本不需要把代码分开在多个文件或者目录里面,同样可以完

  首先,他使用findX(),把一个值写入成员x。然后,使用x的值。这样,x就变成了findXprint之间的数据通道。由于x属于class A,这样程序就失去了模块化的结构。由于这两个函数依赖于成员x,它们不再有明确的输入和输出,而是依赖全局的数据。findXfoo不再能够离开class A而存在,而且由于类成员还有可能被其他代码改变,代码变得难以理解,难以确保正确性。

  如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个 class,而且更加容易理解,不易出错:

代码语言:javascript
复制
 String findX () {
    ...
    x = ...;    return x;
 } void foo () {   int x = findX ();
   print (x);
 }

写可读的代码

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

  实际上,真正优雅可读的代码,是几乎不需要注释的。如果你发现需要写很多注释,那么你的代码肯定是含混晦涩,逻辑不清晰的。其实,程序语言相比自然语言,是更加强大而严谨的,它其实具有自然语言最主要的元素:主语,谓语,宾语,名词,动词,如果,那么,否则,是,不是,…… 所以如果你充分利用了程序语言的表达能力,你完全可以用程序本身来表达它到底在干什么,而不需要自然语言的辅助。

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

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

  1、使用有意义的函数和变量名字。如果你的函数和变量的名字,能够切实的描述它们的逻辑,那么你就不需要写注释来解释它在干什么。比如:

代码语言:javascript
复制
// put elephant1 into fridge2put (elephant1, fridge2);

  由于我的函数名put,加上两个有意义的变量名elephant1fridge2,已经说明了这是在干什么(把大象放进冰箱),所以上面那句注释完全没有必要。

  2、局部变量应该尽量接近使用它的地方。有些人喜欢在函数最开头定义很多局部变量,然后在下面很远的地方使用它,就像这个样子:

代码语言:javascript
复制
void foo () {  int index = ...;
  ...
  ...
  bar (index);
  ...
}

  由于这中间都没有使用过index,也没有改变过它所依赖的数据,所以这个变量定义,其实可以挪到接近使用它的地方:

代码语言:javascript
复制
void foo () {
  ...
  ...  int index = ...;
  bar (index);
  ...
}

  这样读者看到bar (index),不需要向上看很远就能发现index是如何算出来的。而且这种短距离,可以加强读者对于这里的“计算顺序”的理解。否则如果 index 在顶上,读者可能会怀疑,它其实保存了某种会变化的数据,或者它后来又被修改过。如果 index 放在下面,读者就清楚的知道,index 并不是保存了什么可变的值,而且它算出来之后就没变过。

  为了避免 IDE 把这些手动调整好的换行弄乱,很多 IDE(比如 IntelliJ)的自动格式化设定里都有“保留原来的换行符”的设定。如果你发现 IDE 的换行不符合逻辑,你可以修改这些设定,然后在某些地方保留你自己的手动换行。

  说到这里,我必须警告你,这里所说的“不需注释,让代码自己解释自己”,并不是说要让代码看起来像某种自然语言。有个叫 Chai 的 JavaScript 测试工具,可以让你这样写代码:

代码语言:javascript
复制
expect (foo) .to.be.a ('string');
expect (foo) .to.equal ('bar');
expect (foo) .to.have.length (3);
expect (tea) .to.have.property ('flavors') .with.length (3);

  这种做法是极其错误的。程序语言本来就比自然语言简单清晰,这种写法让它看起来像自然语言的样子,反而变得复杂难懂了。

写简单的代码

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

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

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

  • 避免使用自增减表达式(i++,++i,i--,--i)。

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

  其实这两个表达式完全可以分解成两步,把读和写分开:一步更新i的值,另外一步使用i的值。比如,如果你想写foo (i++),你完全可以把它拆成int t = i; i += 1; foo (t);。如果你想写foo (++i),可以拆成i += 1; foo (i); 拆开之后的代码,含义完全一致,却清晰很多。到底更新是在取值之前还是之后,一目了然。

  有人也许以为i++或者++i的效率比拆开之后要高,这只是一种错觉。这些代码经过基本的编译器优化之后,生成的机器代码是完全没有区别的。自增减表达式只有在两种情况下才可以安全的使用。一种是在 for 循环的 update 部分,比如for (int i = 0; i < 5; i++)。另一种情况是写成单独的一行,比如i++;。这两种情况是完全没有歧义的。你需要避免其它的情况,比如用在复杂的表达式里面,比如foo (i++)foo (++i) + foo (i),…… 没有人应该知道,或者去追究这些是什么意思。

  • 永远不要省略花括号。

  很多语言允许你在某种情况下省略掉花括号,比如C,Java 都允许你在 if 语句里面只有一句话的时候省略掉花括号:

代码语言:javascript
复制
if (...) 
  action1();

  咋一看少打了两个字,多好。可是这其实经常引起奇怪的问题。比如,你后来想要加一句话action2()到这个 if 里面,于是你就把代码改成:

代码语言:javascript
复制
if (...) 
  action1();
  action2();
  • 为了美观,你很小心的使用了action1()的缩进。

  咋一看它们是在一起的,所以你下意识里以为它们只会在 if 的条件为真的时候执行,然而action2()却其实在 if 外面,它会被无条件的执行。我把这种现象叫做“光学幻觉”(optical illusion),理论上每个程序员都应该发现这个错误,然而实际上却容易被忽视。

  那么你问,谁会这么傻,我在加入action2()的时候加上花括号不就行了?可是从设计的角度来看,这样其实并不是合理的作法。首先,也许你以后又想把action2()去掉,这样你为了样式一致,又得把花括号拿掉,烦不烦啊?其次,这使得代码样式不一致,有的 if 有花括号,有的又没有。况且,你为什么需要记住这个规则?如果你不问三七二十一,只要是 if-else 语句,把花括号全都打上,就可以想都不用想了,就当C和Java没提供给你这个特殊写法。这样就可以保持完全的一致性,减少不必要的思考。

  有人可能会说,全都打上花括号,只有一句话也打上,多碍眼啊?然而经过实行这种编码规范几年之后,我并没有发现这种写法更加碍眼,反而由于花括号的存在,使得代码界限明确,让我的眼睛负担更小了。

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

  利用操作符的优先级来减少括号,对于1 + 2 * 3这样常见的算数表达式,是没问题的。然而有些人如此的仇恨括号,以至于他们会写出2 << 7 - 2 * 3这样的表达式,而完全不用括号。

  这里的问题,在于移位操作<<的优先级,是很多人不熟悉,而且是违反常理的。由于x << 1相当于把x乘以2,很多人误以为这个表达式相当于(2 << 7) - (2 * 3),所以等于 250。然而实际上<<的优先级比加法+还要低,所以这表达式其实相当于2 << (7 - 2 * 3),所以等于4!

  解决这个问题的办法,不是要每个人去把操作符优先级表给硬背下来,而是合理的加入括号。比如上面的例子,最好直接加上括号写成2 << (7 - 2 * 3)。虽然没有括号也表示同样的意思,但是加上括号就更加清晰,读者不再需要死记<<的优先级就能理解代码。

防止过度工程

  人的脑子真是奇妙的东西。虽然大家都知道过度工程(over-engineering)不好,在实际的工程中却经常不由自主的出现过度工程。我自己也犯过好多次这种错误,所以觉得有必要分析一下,过度工程出现的信号和兆头,这样可以在初期的时候就及时发现并且避免。

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

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

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

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

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

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

  1. 先把眼前的问题解决掉,解决好,再考虑将来的扩展问题。
  2. 先写出可用的代码,反复推敲,再考虑是否需要重用的问题。
  3. 先写出可用,简单,明显没有 bug 的代码,再考虑测试的问题。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-08-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java学习网 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
NLP 服务
NLP 服务(Natural Language Process,NLP)深度整合了腾讯内部的 NLP 技术,提供多项智能文本处理和文本生成能力,包括词法分析、相似词召回、词相似度、句子相似度、文本润色、句子纠错、文本补全、句子生成等。满足各行业的文本智能需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档