关键要点
·由于软件开发过程具有复杂性,所以它很难被理解。
·正是这种复杂性,导致了许多来源不明的信条和直觉。
·最近一项对软件开发过程的研究结果挑战了许多普遍持有的观点。
·一些不太容易理解的研究结果揭示了开发过程中意想不到的力量。
·在软件开发中,非技术因素对整个项目的影响往往胜过技术因素。
最近,我看到了一项关于项目中所使用的编程语言和代码质量相关性方面的研究。?我非常感兴趣,因为研究结果和我预想的截然相反。一方面,这项研究可能有缺陷,另一方面,许多在软件开发中已确立的实践和信念来源不明。我们遵守这些实践和信念是因为“每个人”都在这样做,或者它们被认为是最佳实践,或者它们是由布道者之类的权威人士所宣传的。它们是真的有用还是只是传说?如果我们看看具体数据,结果会怎样呢?我查阅了几篇相关的文献,结果都令人惊讶。
软件系统对我们的经济社会运行起到非常重要的作用,但令人吃惊的是,关于开发过程的科学研究却非常稀缺。其中一个可能的原因是拥有软件开发过程的公司并不急于让研究者进入,因为软件开发过程非常昂贵,让研究者在实际项目上做实验研究不太现实。最近,像GitHub或GitLab这样的公共代码库改变了这种情况,提供了易于访问的数据,使得越来越多的研究人员试图挖掘这些数据。
最早的一项基于公共存储库数据的研究于2016年发表,题为“理解编程语言对代码质量的影响的大型生态系统研究”。它试图验证这样一种信念——几乎普遍认为这是理所当然的——即某些编程语言产生的代码质量高于其他语言。研究人员正在寻找一种编程语言与缺陷的数量和类型之间的关系。对用17种语言开发的729个GitHub项目中与bug相关的提交的分析确实显示了预期的相关性。值得注意的是,像TypeScript、Culjule、Haskell、Ruby和Scala这样的语言比C、C++、Objto-C、JavaScript、PHP和Python更容易出错。
一般来说,函数式语言和静态类型语言比动态类型语言、脚本语言或过程语言更不容易出错。有趣的是,缺陷类型与语言的相关性高于缺陷数量与语言的相关性。总的来说,这一结果并不令人惊讶,证实了大多数社区所相信的内容都是正确的。这项研究很受欢迎,被广泛引用。需要注意的是,这一结果是统计的,当解释统计结果时需要小心。统计意义并不总是意味着实际意义,正如作者警告的那样,相关性不是因果关系。这项研究的结果并不意味着(尽管许多读者已经这样解释过)如果你把C改成Haskell,代码中的bug就会减少。无论如何,这篇论文至少提供了数据支持的论据。
但故事还未结束。由于科学方法的一个基石就是复现,一组研究人员试图从2016年开始复现这项研究。在纠正了原论文中发现的一些方法上的缺陷后,他们于2019年将新的研究成果发表在《编程语言对代码质量的影响》一文中。
但这一复现远未成功,原稿中的大部分内容都未被复现。尽管有些相关性仍具有统计学上的意义,但从实际的角度来看,意义并不显著。换句话说,如果我们查看数据就会发现,就BUG数量而言,我们选择哪种编程语言似乎无关紧要。不相信?让我们看另一篇论文。
2019年的一篇论文《理解真实世界中GO语言的并发缺陷》,重点讨论了用Go开发的项目中的并发缺陷。Go是一种由Google开发的现代编程语言,它被特别设计成使并发编程更容易和更不容易出错。尽管Go提倡使用消息传递并发性,因为它不太容易出错,但它同时提供了消息传递和共享内存同步并发性的机制,因此如果要比较这两种方法,这是一种自然的选择。研究人员分析了在Docker、Kubernetes和gRPC等六个流行的开源Go项目中发现的并发错误。这一结果甚至让作者也感到困惑:
“令人惊讶的是,我们的研究表明,与共享内存一样,消息传递也很容易产生并发bug,有时甚至会产生更多的bug。”
尽管到目前为止我们看到的研究结果表明,编程语言的进步与代码缺陷关系不大,但还有另一种解释。
让我们看一看另一项研究——20世纪80年代初进行的慕尼黑经典出租车实验,虽然这项研究与道路安全无关,但研究人员也遇到了类似的非直观结果。20世纪80年代,德国汽车制造商开始在汽车上安装第一个ABS(防抱死制动系统)。由于防抱死制动系统(ABS)使汽车在制动过程中更加稳定,因此人们自然期望它能提高道路上的安全性。研究人员想知道到底在安全性上能有多少提高。他们与一家出租车公司合作,该公司计划在他们车队的一部分出租车上安装防抱死制动系统。选择了3000辆出租车,在随机选择的一半出租车上安装了ABS。研究人员已经观察这些汽车3年了。之后,他们比较了有ABS组和没有ABS组的事故率。结果有些令人惊讶,两者在事故率上几乎没有什么区别,甚至装有防抱死制动系统的汽车,发生事故的可能性会稍高一些。
正如对Go语言中错误率和并发错误的研究一样,理论上应该有所不同,但数据显示并非如此。在ABS实验中,研究人员收集了额外的数据。首先,汽车被装上了一个黑匣子,黑匣子可以收集诸如速度和加速度之类的信息。其次,观察者被指派给司机,记录他们在路上的行为。这些数据构成的情景十分清晰:安装了防抱死制动系统的汽车司机们改变了他们在路上的行为。司机们意识到现在他们对汽车的控制更好了,停车距离也更短了,司机们开始开得更快、转弯更急甚至主动追尾。
对这一现象的解释是基于心理学中目标风险的概念,即人们的行为使总体风险(称为目标风险)处于一个恒定的水平。当环境发生变化时,人们会调整自己的行为,使风险水平保持不变。在汽车上安装防抱死制动系统降低了驾驶的风险,因此为了弥补这一变化,驾驶员开始更加冒险地驾驶。在其他领域也发现了类似的风险补偿。孩子们在玩带有保护装置的运动时会冒更大的身体风险,带儿童保护盖的药瓶会让父母对药品更不小心,降落伞上装有更好的撕裂绳会在让人更加不及时地拉动它。
让我们回到对代码质量的研究上来。研究人员在分析什么?提交到代码存储库的代码。开发人员何时提交代码?当他确信代码质量可以接受时。换句话说,当提交的错误代码的风险处于合理水平时,他们就会提交代码。当开发人员切换到不易出错的语言时会发生什么?她很快就会注意到,她现在可以编写更少的测试,花更少的时间检查代码,并跳过一些质量检查,同时保持提交低质量代码的相同风险。就像安装了防抱死制动系统的驾驶员一样,她使自己的行为适应了新的情况,因此目标风险与以前一样。每一个开发人员都有一个代码质量的内部标准,目标是在这个标准之下提交代码的风险。请注意,目标风险和标准在开发人员之间会有所不同,但研究表明,平均而言,不同语言的开发人员之间的风险和标准是相同的。
那么接下来的问题就自然而然变成了:有没有其他已经构建好的技术来帮助提高代码质量呢?通过查阅文献我找到了两种方法:结对编程和代码审查。它们能像常说地那样工作吗?嗯,是又不是。事实证明情况有些复杂。在这两种情况下,都有一些研究验证了方法的有效性。
让我们来一份关于结对编程的元分析实验:结对编程的有效性:一份元分析。结对编程真的能提高代码质量吗?分析表明结对编程对质量的整体影响很小,且有显著的正向影响。小小的积极影响听起来有点令人失望,但这不是故事的结局。
“更详细的证据表明,当编程任务复杂度较低时,结对编程比单独编程要快,当任务复杂度较高时,结对编程能产生更高质量的代码解决方案。对于复杂的任务来说,更高的质量是以付出更大的努力为代价的,而对于简单的任务来说,缩短完成时间是以显著降低质量为代价的。”
当运用代码审查这一方法时,通常研究的结果与结对编程的结果相一致。但是代码审查的好处并不是我预期的那样体现在提前进行缺陷检测方面。正如《微软代码审查实践研究——现代代码审查的期望、结果和挑战》的作者总结的那样:
“我们的研究表明,尽管发现缺陷仍然是进行评审的主要动机,但评审对缺陷的关注比预期的要少,相反,评审提供了额外的好处,如知识传递、提高团队意识,以及为问题创建替代性的解决方案。”
接下来,一个自然而然的问题就是为什么科学研究的结果与社会普遍信念之间有如此巨大的差异?一个可能的原因就是学术界和实践者之间的鸿沟,以至于研究成果很传递到到开发者手中。但这只是原因之一而已。
在20世纪80年代中期,Fred Brooks发表了著名的论文“没有银弹-软件工程的本质和事故”。在引言中,他将软件项目比作狼人
“熟悉的软件项目有这样的特点(至少在非技术经理看来是这样),通常是简单的,但却有可能成为一个缺少时间表、预算膨胀和有缺陷产品的怪物。因此,我们听到人们迫切需要一种银弹,一种能使软件成本像计算机硬件成本那样迅速下降的东西。”
他认为,由于软件开发的本质,在软件开发中没有银弹。它本质上是一种复杂的努力。在20世纪80年代,大多数软件运行在一台单核处理器的机器上,互联网还处于起步阶段,智能手机还在遥远的未来,没有人听说过虚拟化或云。Brooks主要写的是技术复杂性,现在我们更加意识到软件开发中涉及的社会、心理和业务过程的复杂性。
自Brooks的文章发表以来,这种复杂性也大大增加。开发团队规模更大,往往是分布式的和跨文化的,软件系统与商业和社会组织的联系更紧密。尽管取得了种种进步,软件开发过程仍然极其复杂,有时甚至处于混乱的边缘。我们必须面对不断变化的需求,不断上升的技术复杂性,以及错综复杂的技术、商业和社会力量所产生的令人困惑的非线性反馈回路。我们的大脑自然很难搞清楚在这样的环境下发生了什么。It界充斥着炒作、神话和信仰之争也就不足为奇了。我们迫切地想理解所有的情形,所以我们的大脑就开始做它们真正擅长的事情——寻找模式。
有时模式太好用了,我们看到火星表面的通道,随机点的脸,轮盘赌的规则。一旦我们开始相信某件事,我们就真的对它上瘾了,每一次对我们的信仰的确认都会让我们大脑分泌多巴胺。我们开始保护我们的信仰,结果我们把自己关在回音室里,我们选择会议、书籍、媒体来证实我们所珍视的信仰。随着时间的推移,这些信念逐渐固化在一个几乎没有人敢挑战的教条中。
即使有科学的方法让我们能够以更理性的方式处理复杂性和偏见,也很难预测软件开发等复杂过程中的行为与结果。我们把编程语言改得更好,代码质量却不变,我们引入结对编程或代码复查来提高代码质量,却让我们体验到更低的质量,或者在我们意想不到的领域得到好处。但复杂性也有好的一面——我们可以找到意想不到的杠杆作用点。如果我们想提高代码质量,而不是寻找技术解决方案,比如一种新的编程语言或更好的工具,我们可以专注于改进开发文化,提高质量标准,或者让犯错误的风险更大。
从这个角度来看,可以发现一些不明显的机会。例如,如果一个团队引入了代码评审,它会使开发人员生成的代码对团队中的其他成员更加可见,从而增加提交低质量代码的风险。因此,代码评审应该具有提高提交的代码质量的效果,不仅通过发现评审人员的bug或违反标准的行为,而且通过防止开发人员提交bug。换句话说,为了提高代码的质量,它应该足以让开发人员相信他们的代码正在被审查,即使没有人这样做。
研究的用意还在于,技术因素并不能同心理因素和文化因素割裂开。与许多其它领域一样,基于数据的研究表明,世界并没有按照我们所相信的方式运转。为了检验我们的信念与现实的吻合程度,我们不必等待研究人员进行长期研究。之前我们在某个话题上产生了分歧,双方争论不休。大约半小时后,有人说-我们上网查吧。我们在30秒内解决了分歧。科学思维和一定剂量的怀疑不是留给科学家的,有时上网快速查看就足够了,有时我们需要收集和分析数据。但是,如何在软件开发实践中引入更多的合理性是一个广泛的话题,也许值得另作一篇文章了。
作者:咖啡猫