12 KiB
04 | 代码规范的价值:复盘苹果公司的GoToFail漏洞
我们在上一讲中讨论了一个优秀的程序员都需要具备哪些良好的品质,第一点就是要熟练掌握一门编程语言。
作为每天都要和代码打交道的人,光是熟练掌握还不够。我们需要像文字写作者一样,对代码有一种“洁癖”,那就是强调代码的规范化。
什么是编码规范?
要回答为什么需要编码规范,我们首先要了解编码规范指的是什么。
编码规范指的是针对特定编程语言约定的一系列规则,通常包括文件组织、缩进、注释、声明、语句、空格、命名约定、编程实践、编程原则和最佳实践等。
一般而言,一份高质量的编码规范,是严格的、清晰的、简单的,也是权威的。但是我们有时候并不想从内心信服,更别提自觉遵守了。你可能想问,遵循这样的约定到底有什么用呢?
编码规范可以帮我们选择编码风格、确定编码方法,以便更好地进行编码实践。 简单地说,一旦学会了编码规范,并且严格地遵守它们,可以让我们的工作更简单,更轻松,少犯错误。
这个问题弄明白了,我们就能愉快地遵守这些约定,改进我们的编程方式了。
规范的代码,可以降低代码出错的几率
复杂是代码质量的敌人。 越复杂的代码,越容易出现问题,并且由于复杂性,我们很难发现这些隐藏的问题。
我们在前面已经讨论过苹果公司的安全漏洞(GoToFail漏洞),接下来再来看看这个bug的伪代码。这个代码很简单,就是两个if条件语句,如果判断没问题,就执行相关的操作。
if ((error = doSomething()) != 0)
goto fail;
goto fail;
if ((error= doMore()) != 0)
goto fail;
fail:
return error;
这段代码没有正确地使用缩进和大括号,不是一段符合编码规范的源代码。 如果我们使用符合规范的编码方式,这个安全漏洞就自然消失了。你可以看到,下面的代码里,我给if语句中加了大括号,代码看起来一下子就简单很多了。
if ((error = doSomething()) != 0) {
goto fail;
goto fail;
}
if ((error= doMore()) != 0) {
goto fail;
}
fail:
return error;
所以在编码的时候,我们应该尽量使代码风格直观、逻辑简单、表述直接。 如果遵守编码规范,我们就可以更清楚、直接地表述代码逻辑。
规范的代码,可以提高编码的效率
还记得我们在前面讨论过代码“出道”的重重关卡吗?这些关卡,构成了代码制造的流水线。优秀的代码,来源于优秀的流水线。
如果我们都遵守相同的编码规范,在每一道关卡上,会产生什么样的质变呢?
在程序员编写代码这道关,如果我们规范使用缩进、命名、写注释,可以节省我们大量的时间。比如,如果使用规范的命名,那么看到名字我们就能知道它是一个变量,还是一个常量;是一个方法,还是一个类。
在编译器这道关,我们可以避免额外的警告检查,从而节省时间。还记得我们前面讨论过的GCC关于正确使用缩进的编译警告吗? 如果有编译警告出现,我们一般都要非常慎重地检查核对该警告有没有潜在威胁。这对我们的精力和时间,其实是不必要的浪费。
还记得GCC由于老旧的编程风格的原因,不支持无法访问代码编译错误吗? 过度自由的编码风格,有时候甚至会阻碍编译器开发一些非常有用的特性,使得我们无心的过失行为越积累越不好解决。
在代码评审这道关,如果我们不遵守共同的编码规范,这多多少少会影响评审者阅读代码的效率。为什么呢?因为评审者和编码者往往有着不一样的审美偏好。一条评审意见,可能要花费评审者很长时间来确认、评论。 然后,源代码编写者需要分析评审意见,再回到流水线的第一关,更改代码、编译、测试,再次提交评审,等待评审结果。
审美偏好一般都难以协调,由此导致的重复工作让编码的效率变得更低了。
在代码分析这道关,编码规范也是可以执行检查分析的一个重要部分。类似于编译器,如果有警告出现,分析警告对我们的精力是一种不必要的浪费; 如果过度自由,同样会阻碍代码分析工具提供更丰富的特性。
只要警报拉响,不管处在哪一个关卡,源代码编写者都需要回到流水线的第一关,重新评估反馈、更改代码、编译代码、提交评审、等待评审结果等等。每一次的返工,都是对时间和精力的消耗。
总结一下,在代码制造的每一道关卡,规范执行得越早,问题解决得越早,整个流水线的效率也就越高。
前一段时间,阿里巴巴发表了《阿里巴巴Java开发手册》。我相信,或许很快,执行阿里巴巴Java编码规约检查的工具就会出现,并且成为流水线的一部分。 对于违反强制规约的,报以错误;对于违反推荐或者规约参考的,报以警告。这样,流水线才会自动促进程序员的学习和成长,修正不符合规范的编码。
规范的代码,降低软件维护成本
代码经过重重关卡,好不容易“出道”了,这样就结束了吗?
恰恰相反,“出道”之后,才是代码生命周期的真正开始。
如果是开源代码,它会面临更多眼光的挑剔。即使是封闭代码,也有可能接受各种各样的考验。"出道”的代码有它自己的旅程,有时候超越我们的控制和想象。在它的旅程中,会有新的程序员加入进来,观察它,分析它,改造它,甚至毁灭它。软件的维护,是这个旅程中最值得考虑的部分。
有统计数据表明,在一个软件生命周期里,软件维护阶段花费了大约80%的成本。这个成分,当然包括你我投入到软件维护阶段的时间和精力。
举例来说吧,让我们一起来看看,一个Java的代码问题,在OpenJDK社区会发生什么呢?
在Java的开发过程中,当需要代码变更时,我们需要考虑一个问题:使用这些代码的应用是否可以像以前一样工作?
一旦出现了问题,一般有两种可能:要么是Java的代码变更存在兼容性问题,要么存在应用使用Java规范不当的问题。这就需要确认问题的根源到底是什么。
由于OpenJDK是开源代码,应用程序的开发者往往需要调试、阅读源代码。阅读源代码这件事情,在一定程度上,类似于代码评审的部分工作。如果代码是规范的,那么他们的阅读心情就会好一些,效率也就更高。
如果发现了任何问题,可以提交问题报告。问题报告一般需要明确列出存在的具体问题。 对于问题报告,也会有专门的审阅者进行研究分析,这个问题描述是否清晰?它是不是一个真正的问题?由谁解决最合适?
很多情况下,报告的审阅者也需要阅读、调试源代码。良好的编码规范,可以帮助他们快速理解问题,并且找到最合适的处理人员。
如果确定了问题,开发人员或者维护人员会进一步评估、设计潜在的解决方案。如果原代码的作者不能提供任何帮助,比如已经离职,那么他们可以依靠的信息,就只有代码本身了。
你看,这个代码问题修改的过程重包含了很多角色:代码的编写者、代码的使用者、问题的审阅者以及问题的解决者, 这些角色一般不是同一个人。在修改代码时,不管我们是其中的哪一个角色,遵守规范的代码都能够节省我们的时间。
很多软件代码,其生命的旅程超越了它的创造者,超越了团队的界限,超越了组织的界限,甚至会进入我们难以预想的领域。即使像空格缩进这样的小问题,随着这段代码的扩散,以及接触到这段代码人数的增加,由它造成的效率问题也会对应的扩散、扩大。
而严格遵守共同的编码规范,提高代码的可读性,可以使参与其中的人更容易地理解代码,更快速地理解代码,更快速地解决问题。
编码规范越使用越高效
除了上面我们说道的好处,编码规范还有一个特点,就是越使用越高效。
比如我们小时候都背诵过乘法口诀,如果我问你,3乘3得几? 我相信,你立即就会告诉我,答案是9。 不管这时候你是在开车、还是在走路;是在吃饭,还是在玩游戏。
如果我问你,13乘以23,结果是多少? 除非你经过非常特殊的训练,你不会立即就有答案,甚至你走路的时候,不停下脚步,就算不出这个结果。
如果我问一个还没学过乘法的小孩子呢? 3乘3的算术,对于小孩子,也许是一个不小的难题。
对于背诵过乘法口诀的我们来说,3乘3的算术根本就不需要计算,我们的大脑可以快速地、毫不费力地、无意识地处理这样的问题。 这种系统是我们思维的快系统。 快系统勤快、省力,我们喜欢使用它。
而对于13乘以23的算术,我们的大脑需要耗费脑力,只有集中注意力,才能运算出来。这种系统是我们思维的慢系统。慢系统懒惰、费劲,我们不愿意使用它。
快系统和慢系统分工协作,快系统搞不定的事情,就需要慢系统接管。 快系统处理简单、固定的模式,而慢系统出面解决异常状况和复杂问题。
比如上面苹果公司安全漏洞的那个例子,如果我们像乘法表一样熟练使用编码规范,一旦遇到没有使用大括号的语句,我们立即就会非常警觉。 因为,不使用大括号的编码方式不符合我们习以为常的惯例,快系统立即就能判别出异常状况,然后交给慢系统做进一步的思考。 如果我们没有养成编码规范的习惯,我们的快系统就会无视这样的状况,错失挽救的机会。
所以,我们要尽早地使用编码规范,尽快地培养对代码风格的敏感度。 良好的习惯越早形成,我们的生活越轻松。
小结
对于编码规范这件事,我特别想和你分享盐野七生在《罗马人的故事》这套书里的一句话:“一件东西,无论其实用性多强,终究比不上让人心情愉悦更为实用。”
严格地遵守编码规范,可以使我们的工作更简单,更轻松,更愉快。 记住,优秀的代码不光是给自己看的,也是给别人看的,而且首先是给别人看的。
你有什么编码规范的故事和大家分享吗? 欢迎你在留言区写写自己的想法,我们可以进一步讨论。也欢迎你把今天的文章分享给跟你协作的同学,看看编码规范能不能让你们之间的合作更轻松愉快。
一起来动手
下面的这段代码,我们前面用过一次,我稍微做了点修改。我们这次重点来看编码的规范,有哪些地方你看着不顺眼,你会怎么改进?
package com.example;
import java.util.Collections;
import java.util.List;
import javax.net.ssl.SNIServerName;
class ServerNameSpec {
final List<SNIServerName> serverNames;
ServerNameSpec(List<SNIServerName> serverNames) {
this.serverNames = Collections.<SNIServerName>unmodifiableList(serverNames);
}
public String toString() {
if (serverNames == null || serverNames.isEmpty())
return "<no server name indicator specified>";
StringBuilder builder = new StringBuilder(512);
serverNames.stream().map((sn) -> {
builder.append(sn.toString());
return sn;
}).forEachOrdered((_item) -> {
builder.append("\n");
});
return builder.toString();
}
}
你也可以把这篇文章分享给你的朋友或者同事,一起来讨论一下这道小小的练习题。