作为程序员,我们都听说过“高质量代码”这个词,但我们是否想过这其中真正的定义?
我在业界已经做了13+年的程序员,经历大小不同的四家公司(微软,谷歌,Sumo Logic,到现在的 Leap.ai)。从这些亲身经历中我总结出一套代码质量的衡量标准,我称之为代码质量闯关:
  • 正确性
  • 高效性
  • 可读性
  • 延展性
今天,在这篇文章里,我们会详细探讨各标准。希望这个系列对程序员们,特别是职业初期的,有所帮助。
代码质量第一关:正确性
写正确的代码对一个程序员来说是最基本的要求。这听着好像很明显,但其实在实际工作中做到并不像说的那样容易。

以我常用的一个面试题来举个例子。在 Microsoft Excel(或其他许多报表产品)中,列的排序是从A到Z,然后是 AA,AB,等。这个面试题就是要求写一个函数,将列名转化为列数。也就是说,A变成1,B变成2,C变成3,等等。
我用这道题面试过的人,几乎所有的都能很快的指出这题的本质是26进制转化,然后进入代码编写阶段。但是,能真正将这个函数编写正确的,少之又少。你不信?咱们来看看下面这个C++的实现。
int ConvertColumnName(const char* name) {
  int result = 0;
  char* index = name;
  while (*index) {
    result = result * 26 + (*index - ‘A’ + 1);
    index++;
  }
  return result;
}
你能找出其中的问题吗?如果看不出来,就再试试。:)
这段代码有好几个问题:
  • const char* 是C语言的字符串。在C++程序中用C语言的字符串是很危险的。C语言的字符串都是0结尾的,但C++的不一定。在C++里,应该尽量使用std::string。
  • 指针和引用有可能是NULL,如果这里的输入指针是NULL,这段代码会崩溃。你有没有经历过,凌晨3点需要紧急解决服务器崩溃,用户无法登录你的网站,最后发现原因就是一个NULL指针?
    • 也许你会说,“但我一定会进行输入检验,保证调用程序不输入NULL指针”
    • 对不起,现实生活里,你保证不了。你的代码一旦写完,你根本就不知道谁在什么情况下会调用你的代码。
  • 上面这段程序能处理小写字符串吗?
  • 非字母呢?
  • Unicode?
  • 如果输入字符串是7个A呢?是的,整数溢出 - 你的代码能处理这个吗?
当你在写代码的时候,请记住墨菲定律:凡是可能出错的事必定会出错。在程序中,墨菲定律是100%正确。这也就意味着,作为程序员,你的责任就是能够处理任何输入。无论怎样都别给你的用户带来意外。
要达到这一点,你必须对程序的可测试性有深刻了解,并且广泛实施单元测试。
代码质量第二关:高效性
恭喜你,你现在已经实现第一阶段,你写的程序已经基本正确。(顺便提一句,世界上不存在没有bug的程序,所以基本正确已经是很难能可贵的了。)

你兴奋的将你的代码推上线,咣当!你的网站挂了。你立刻调出服务器日志,准备海底捞针的找出问题所在。(请自己脑补,大汗淋漓,心慌意乱的景象。)终于最后锁定是你写的一个函数,无法处理所有的调用请求,导致超时。
欢迎进入高效性的领地。
什么是高效性?这跟大O符号(Big-O Notation)是一样的吗?答案是,相近但不相同。
让我用一个例子来说明。最快的排序算法是什么?你也许说,快速排序(Quicksort)是最快的排序算法,平均情况下是O(n * log n)。
在我们深入分析之前,让我们先回顾一下大O符号的定义。总的说来,大O符号表示的是一个算法所需的最多时间。如果一个算法是O(n * log n),这意味着对无论什么输入量 n,算法需用时间一定小于 k * n * (log n),在这里的 k 是个特定常数。当 n 足够大时,我们知道O(n * log n)的算法一定比O(n^2)的算法快。
但是,在实际情况里,讨论算法速度有两个必须考虑的参数。
  • 输入量 n 十分重要。比方说,如果我们要排序一个含有6个元素的数组,O(n^2)的冒泡排序很可能比O(n * log n)的快速排序反而要快的多。
  • 常数 k 其实也很重要。在计算机理论分析中,我们从来不考虑 k 的实际取值,但是在实际工作中,它非常关键。对于所有的n < 1000的输入量,一个k = 1的O(n^2)算法其实是比一个k = 1000的O(n)算法要快的。用一个实际工作中真实的例子来说,在内存里的冒泡排序可能比在硬盘上的合并排序(Mergesort)要快。
在这里,我们的谈论话题还仅局限于时间上的高效性。实际工作中,我们要考虑的还有,空间效率,能耗效率,算法收敛效率,用户体验效率,等等。鉴于篇幅限制,对这些方面我在这里不再深入展开,但总的说来,我要强调一点:程序的高效性是相对于其存在的背景而言的。我们的责任是,在相应的限制条件下,写最高效的代码。
还有一点要注意,高效性做过了也不是好事。这通常被称为“过早优化“,需要避免。只在需要优化的时候才优化,如果不确定,就先实现最简单的方法,然后测试用数据来决定是否需要优化。
代码质量第三关:可读性
好吧,现在你写的程序已经是既正确也高效。下一个挑战是让你的代码,在别人眼里,易读易懂。
现代的软件工程总是由团队完成的。这意味着,你写的代码不是你的,而是你的团队的(甚或是别的团队的)。对于你而言,能让你的队友维护你的代码是件大好事。你不希望在你休假的时候接到队友的电话,说看不懂你的代码,需要你马上上线修改代码来解决一个问题,对不对?这种事情其实在很多程序员身上都发生过。我甚至有过在离开一家公司一年之后,仍被召唤解释代码的经历 - 那个经历不怎么愉快。
在业界,一种广泛被采用的方法叫做“编程风格指南”。整个团队采用一套公用的,大家认可的编程风格。在这里,我强烈推荐谷歌编程风格指南。这个指南被整个谷歌公司内的程序员所遵守(有时甚至过于呆板的遵守),也同时已经成为很多其他公司所接受的规范。这一套指南涵盖了许多种通用编程语言。
谷歌的编程风格指南写的非常好,我在这里就不再重复其内容。只有一点我想强调一下,那就是,如果你的团队已有一套编程风格指南,那么更重要的是采用你团队现有的指南,哪怕其中有跟业界通行的风格不同的地方。你的团队的代码要风格一致,这点更重要。

采用一套编程风格指南是一个好的开始,但光凭这个还远远不够。程序可读性里另一点很关键的是优秀的注释。好的注释不应该简单重复程序,而是解释程序背后的想法和背景。
在这里,我故意挑选了一个特别简单的例子。麻雀虽小,五脏俱全。例子虽然简单,但是其中的道理一样深刻。对比下面这两行注释:
  int index = -1;  // 初始化为-1;无用的注释
对比,
  int index = -1;  // 初始化为无效值;有用的注释
嫌这个例子过于简单?那么请看谷歌开源的 LevelDB 头文件,其中有非常好的注释,大家可以借鉴。这个程序的原作者是 Jeff Dean 和 Sanjay Ghemawat(谷歌的两位 Senior Fellow)。
请记住,程序可读性的最终评判是你的队友。你的目标是让你的队友轻松读懂你写的代码(以便你可以放松享受你的休假)。
代码质量第四关:延展性
如果你写的代码是正确的,高效的,可读的,那就意味着你已经是前10%的高手了。接下来的挑战 - 延展性 - 将带领你登顶。
情况时刻在变化,程序随之演变。一个好的程序员能预测到将来会发生的变化,然后在今天的代码中就提前准备应对。这样的代码是“未来兼容”的,这也就是“延展性”。
让我用著名的 MapReduce 为例深入分析。MapReduce 诞生于谷歌搜索团队,起因是很多程序员需要分析大量的数据来解决类似的问题。Jeff Dean 和 Sanjay Ghemawat(对,又是他俩)看到这个需求,决定写一个标准化的架构来解救众程序员于水深火热。
说句老实话,其实这样的标准化的架构可以有很多种,另外学术界也有很多批评认为 Map 和 Reduce 的概念也不是他俩的创新。但是,MapReduce 的成功就在于这个架构标准化的恰到分寸。一方面它特定,特定到谷歌搜索团队可以有效的使用这个架构让他们的工作简化,另一方面它又不是那么特定,不特定到在谷歌搜索之外也有适用之处。很快的,MapReduce 被广泛应用于谷歌和超越谷歌的情景下,成为整个互联网大数据时代的奠基石。
从这里我们可以看到,延展性的关键就在于,延展到什么样的度才是恰到分寸?这个问题的答案,与其说是科学,还不如说是艺术。如果你的代码过于特别,用处就会受到局限,每一次需求改变时,代码也需要改变。如果你的代码过于通用,你的用户仍需要在上面搭建自己的程序才能解决他们的需求,那么你的代码就会难于使用。
要想掌握延展性,需要很多的经验和智慧。对于绝大多数程序员而言,最好的方法莫过于尝试,然后从经验教训中学习。即使在谷歌,我知道至少有四五个架构想试图标准化 MapReduce 链。你瞧,尽管他们有 Jeff 和 Sanjay 就在手边,他们在这件事上也没能一次成功。:)
总结
这四个方面并不完全是独立的。可读性高的程序通常也不容易出错,效率不高的程序基本无法有效延展。所以对于每个程序员而言,目标应该是在这些方面都有所长。但是总的而言,这四关的阶段性是存在的。最好能根据自己的实际情况,想一想自己当下最多的努力是花在哪个方向,然后就可以集中精力把这一关先闯过去。要小心避免越级闯关,前面的基础没打好,后面的也会吃力。
祝各位程序员都能顺利在代码质量上登顶。
// 谷歌编程风格指南
https://github.com/google/styleguide
// 谷歌开源的LevelDB头文件
https://github.com/google/leveldb/blob/master/include/leveldb/table.h
____
有句老话“姜还是老的辣”,毕竟"老程序员"因为时间见多了bug,积累了经验。Google 了一下“老程序员”,出现的结果都是“老程序员的建议和忠告”、“‘老’程序员的人生感悟”,“老程序员总结的xx条经验教训”......
不过小编读了一下,除了技术上的经验教训以外,还有很多来自“老程序员”的思考与职场选择。也许你想提高自己的专业水平、业务能力,但是也对自己的前途、职场发展充满疑问?
让 Leap Advisor 帮你匹配一位最适合的过来人,为你指点迷津吧!
感受传帮带力量,
切入职场快车道。
欢迎体验 Leap Advisor
https://leap.ai/ (或”阅读原文“)
/* 推荐大家电脑操作 */
如果你有自己的职场问题,
也欢迎在公众号中给我留言。
/*LeapdotAI是能聊天的公众号,
也欢迎留言吐槽。*/
Leap.ai 更多干货阅读:
继续阅读
阅读原文