在我的面试工具箱中,有一把锤子是我的最爱。一旦在这道面试题上捕捉到积极信号,无论在其他问题上的反馈多么不靠谱,我都会想法设法把候选人收进来。

期望的结果只有一个,而错误的可能性却有千千万万

这道题是关于滥用防卫性编程的。和大部人的直觉相反,生产中绝大部分所谓防卫性代码(错误处理代码)自身都是垃圾,根本没有存在的必要,而且错误成堆,一旦不幸运行就会制造灾难!绕过这个坑的工程师写出来的代码通常比一般代码要少三分之一,圈复杂度会下降一半(不是瞎掰,我是通过度量前后版本的差异得到这个数据的),而可靠性反而会有惊人的提高。

关于防卫性编程,绝大部分软件工程师的成长经历是这样的:

阶段一, 没有防卫

这个阶段很快就会过去,因为有无数的书本和”前辈”在提示防卫性编码的必要性。

阶段二, 疯狂防卫

无论是模块之间,还是模块内部,处处充满检查,首先是检查参数,其次是检查返回值和出参;最后,给自己写的函数通通加上指示成功失败的返回码。

如果读者诸君有幸参加过代码评审会,对阶段二的代码应该不会陌生,看看到底哪里不对劲?

只管汇报,不管处理,不了了之

这些泛滥的防卫性代码有一个共同的特点–只管检查汇报,不管处理。所谓的处理就是一层一层的return错误码,典型的懒政行为,老子眼界不够高,做不了决定,把皮球丢给上级机关。上级机关面对来自不同下属的千奇百怪的错误码,他也没辙,只好再丢给上上级机关。这样层层上报,一直汇报到习大大。你觉得习大大要维护多大的队伍才能搞得定?人手再多也搞不定,因为用来分析错误的上下文信息已经在层层汇报中丢失了。所以别看费了好大力气汇报错误码,最后除了一死了之或者睁只眼闭只眼也没有更好的办法。

内外不辨,亲疏不分

防卫是应该的,但不应该处处为营。国界线上要有边防军巡逻,但是省界呢?县界呢?设防是有成本的,在软件上就体现为交复杂度税。同一个模块内部,假设A调用B,如果B认为A传给它空指针(空对象)是一个错误,那么A应该设计为永远不会把空指针传递给B,而不是在B处设防。模块内部设防,就好比两口子同床异梦,精力都内耗了。

人格分裂,自相矛盾

设计这种代码的人一方面认为自己思维缜密,步步设防,另一方面认为自己白痴到底,所以要步步设防,左手要防着右手。左右手互搏的代码散布得纷纷扬扬,几乎无法辨认有限的功能代码。

顾头不顾尾,屁股擦不干净

只要评审得足够仔细,总能发现这些防卫性代码的漏洞,要么是文件没关闭,要么是内存没释放。这是必然的,因为一个正常人根本没脑力搞定这么多细节(你要考虑错误处理代码也是层层嵌套,充满了排列组合的)。考虑到天量的路径组合和逻辑的自相矛盾,你也没办法设计测试用例来验证它。这就是为什么真的一不小心命中的话,结局总是很悲惨的原因。写到这里,我想起历史上重大的安全事故调查报告,事故之所以能发生,是因为多道安全闸门同时失效,而软件故障会更加可怕,因为后面的闸门本身就是炸弹。

可惜的是,无数工程师一脚踏入阶段二这个大坑后再也没有爬出来。

我每次评审代码,上手第一刀就是检查错误处理。边界处的错误是否都被检查?错误处理是否遵从系统规定的一致策略?模块内部是否存在周伯通?一旦作者把错误处理的评审意见处理完毕,剩下的代码基本上就身段苗条、脉络清晰了,因为用来实现正常功能的代码真的不会很多。

说了这么多,那么阶段三到底又是什么样的呢?

边界清晰,御敌于国门之外

在模块的边界处,无论别人调用你还是你调用别人,一定要明确约定调用规范,一旦违反规范,立即启用错误处理流程(注意不是防卫流程),不要让错误在自己的模块内部流窜。

不给错误做二传手

如果发现错误,让错误到此为止,非必要不接力传递错误码

当断则断,早死早超生

所有程序的正确运行都是依赖于一定的前提条件的,如果你发现外部模块不工作了,遵从系统的错误处理策略,该报异常就报异常,该立即退出就退出,重要的是搜集好现场的证据。一味的容错处理既不能解决问题还会掩盖问题,导致错误扩散,变形,拖延定位问题的时机。

和谐社会,简单单纯

在模块内部,只有不言自明的约定,没有周伯通似的左右手互搏。好比乌龟,龟壳坚硬,但龟壳内部是一个柔软的世界。

这样的代码,模块内部几乎没有用于实现需求以外的代码,很容易阅读,仅通过黑盒测试就能实现代码覆盖。

阶段三的程序员靠谱在哪里?

有些工程师似乎天生就跨过了阶段二,我遇到一个,当时他还是在校研究生,现在已经创业有成了。我发现这些工程师一般有如下特点:

  • 刨根究底,不满足于似是而非,对自己程序所依赖的外部环境和自己模块的职责有清晰的认识
  • 有全局观念,站在整个软件的角度设计单个模块,站在整个系统的角度设计软件
  • 倾向于写”简单而明显没有错误的程序”
  • 正确的错误处理设计需要对软件和系统,模块和整体软件之间的职责划分有清晰的认识,阶段三是一个合格软件架构师的起点。

发现这样的候选人也不是太麻烦,让他面试的时候随身带一份代码来就可以了(不是每一个工程师都有在github上留代码的习惯)。不过,最有价值的实践是通过代码评审把工程师从阶段二推向阶段三。