在软件开发项目中,常见的争论之一是花费时间来提高软件质量,还是集中精力发布更有价值的功能。通常来说,交付功能的压力占据了主导地位,许多开发人员因此抱怨他们没有时间在架构和代码质量方面进行研究与处理。
贝特里奇头条定律是一句俗语:“任何以问号结尾的头条,都能够用‘不’来回答。” 那些了解我的人不会怀疑我渴望颠覆这条定律的心。但本文将会走得更远,因为它颠覆了问题的本身。这个问题假设了质量和成本之间存在一个共同的权衡。在本文中,我将为读者们解释,这种权衡并不适用于软件开发——高质量的软件实际上生产成本更低。
译注:贝特里奇头条定律(英语:Betteridge’s law of headlines ),该定律以英国科技记者伊恩・贝特里奇(Ian Betteridge)命名,尽管有关该定律之理论的出现早于贝特里奇活跃的年份。与类似的 “定律”(比如墨菲定律)一样,这是一个幽默的格言,而不是字面意义上的事实。
虽然我的大部分文章是针对专业软件开发人员而写的,但在本文中,我不会假设读者具有任何相关软件开发机制的知识背景。我希望这篇文章对任何涉及软件工作的人们而言都有价值,特别是那些作为软件开发团队的客户的受众,比如商业领袖。
正如我在本段标题提到的那样,我们都习惯了在质量和成本之间进行权衡。当我更换智能手机时,我可以选择一个更昂贵的型号,处理器更快、屏幕更好、内存更多。或者我可以为了花更少的钱而放弃其中一些品质。然而,这并非绝对规则,因为我有时候会趁打折促销活动之类的机会用更少的钱买到优质商品。更多的时候,我们对质量有不同的价值观:有些人并没有真正注意到某种手机屏幕是否比其他手机屏幕更好。但在大多数情况下,这一假设是正确的:好货不便宜,便宜没好货。
如果让我来谈论软件质量,请让我首先解释什么是软件质量。这就是第一个复杂问题:有很多东西可以算作软件质量。比如,我可以考虑用户界面:是否能够轻松引导我完成需要完成任务,让我变得更有效率,并驱散挫折感吗?我还可以考虑它的可靠性:它是否包含导致错误和引起挫折感的缺陷呢?另一方面就是它的架构了:源代码是否分为清晰的模块,以便程序员能够轻松地找到并理解他们在本周需要处理代码的那一部分?
当然,上面提到的这三个关于软件质量的例子,并不是一个详尽的列表,但它们足以说明一个重要的观点。如果我是软件的客户或用户的话,我不会去了解他们称之为质量的东西。用户自己就可以判断用户界面是否良好。用户和客户自会注意到缺陷,特别是当它们造成数据被损坏或者使系统暂时无法运行时。但是客户和用户却无法理解软件的架构。
因此,我将软件质量属性划分为 外部 (如用户界面和缺陷)和 内部 (架构)。区别在于,用户和客户可以看到是什么使软件产品具有较高的外部质量,但却无法区分内部质量是较高还是较低。
既然内部质量并不是客户或用户所能看到的,这有什么关系吗?让我们想象一下,Rebecca 和我各自编写了一个应用程序,用来跟踪和预测航班延误。我们的两个应用程序都有相同的基本功能,都有同样优雅的用户界面,并且几乎没有任何缺陷。唯一的区别是,Rebecca 写的内部源代码组织得整齐有序,而我的源代码则写得一团乱麻。另外,还有一个区别是,我编写的应用程序售价是 6 美元,而她编写的是 10 美元售价。
既然客户从来没见过这个应用程序的源代码,而且也不影响应用程序的运行,那为什么还会有人为 Rebecca 的软件额外支付 4 美元呢?更笼统地讲,这应该意味着不值得为更高的内部质量支付更多的钱,不是吗?
换句话说,用成本来换取外部质量是有意义的,但用成本换取内部质量是没有意义的。用户可以判断他们是否愿意花更多的钱来得到更好的用户界面,因为他们可以评估用户界面是否足够好,是否值得支付额外的费用。但是用户并不能看到软件的内部模块结构,更遑论判断它是不是更好。那为什么要为没有效果的东西支付更多的费用呢?既然如此,那为什么软件开发人员应该将更多的时间和精力用来提高他们工作的内部质量呢?
那么为什么软件开发人员会把内部质量当成一个问题呢?程序员大部分时间都花在代码的修改上。即使在一个新系统中,几乎所有的编程都是在现有代码基础上完成的。当我想给软件增加一个新功能时,我的第一个任务就是弄清楚这个功能如何适应现有应用程序的流程。然后,我需要改变这个流程,以让我添加的功能能够适应。我经常需要用到应用程序中已存在的数据,因此我需要了解这些数据代表什么,它如何与周围的数据关联,以及我可能需要为我的新功能添加哪些数据。
所有这些都是关于我对现有代码的理解。但是,软件很难让人理解。逻辑可能会变得很复杂,数据可能很难理解,六个月前用来指代事物的名字可能对 Tony 有意义,但对我来说,就像他离开公司的理由一样神秘。所有这些都是开发人员称之为“ Cruft ”的形式:当前代码和理想情况下代码之间的区别。
译注:Cruft 指的是程序源代码中随时间累积而变得无用的过时垃圾程序代码(美国传统英语字典解释为随时间而增加的有害物质或无用讯息)。随着软件的发展,以及经历了修改 Bug 和重构的若干周期之后,软件的部分代码已不再使用,但这些代码仍然保留在源码中,这种代码称为 Cruft。Cruft 可能是一两行无用的代码,也可能是整个源文件模块。
内部质量的一个主要特点之一是,让我更容易理解应用程序的工作方式,这样我就能看到如何添加内容。如果软件被很好地划分为不同模块的话,我就无需阅读所有 50 万行代码,就可以在几个模块中快速找到我要找的那几百行代码。如果我们把精力放在清晰的命名上,那么我就可以快速理解代码各个部分的用途,而不必纠结于细节。如果数据合理地遵循底层业务的语言和结构,我就可以很容易地理解它如何与客户服务代表那里得到的请求相关联。Cruft 增加了我理解如何改变的时间,还增加了我犯错误的可能性。如果我发现了自己的错误,那么我就会浪费更多的时间,因为我还必须了解这一错误是什么,以及如何纠正这个错误。如果我没有发现这些错误,那么我们就遇到产品缺陷,然后将会有更多的时间花在未来的修复上。
技术债务是 Cruft 的一个常见的比喻。添加功能的额外成本就跟支付利息一样。清理 Cruft 就像偿还本金一样。虽然这一比喻很有用,但它确实鼓励许多人相信,与实际情况相比,Cruft 更容易测量和控制。
我的改变也会影响到未来。我可能找到了一个快速加入这个功能的方法,但这与程序的模块化结构背道而驰,如此一来这就增加了 Cruft。如果我选择这条路的话,那么我今天就可以让它加快速度,但在未来的几周或几个月里,我会让其他所有必须处理这段代码的人都放慢速度。一旦团队中的其他成员做出相同的决定,一个易于修改的应用程序就会快速累积到每个微小的更改都要花费数周时间的地步。
这里我们看到了内部质量对用户和客户很重要的线索。更好的内部质量使得添加新功能变得更容易,因此开发速度更快,售价也更便宜。Rebecca 和我各自编写的应用程序现在可能有同样功能,但在接下来的几个月里,Rebeca 由于她编写的程序内部质量之高,得以能够做到每周添加新功能;而我却被卡住了,一直在努力突破瓶颈,就为了推出一个新功能。我无法与 Rebecca 的开发速度竞争,很快,她的软件就比我的软件功能强大得多,然后我所有的客户都卸载了我的应用程序,转而购买 Rebecca 的应用程序,即使她提高售价也在所不惜。
内部质量的基本作用是降低未来变更的成本。但是编写优秀的软件需要额外的努力,而这在短期内确实会带来一些成本。
可视化的一种方法是使用如下图的伪图,其中我绘制了软件的累积功能与生成它的时间(以及成本)的关系。对于大多数软件来讲,曲线看起来如下图所显示的那样。
这就是槽糕的内部质量所造成的后果。最初进展很快,但随着时间的推移,添加新功能变得越来越困难。即使进行很小的更改也需要程序员理解大量的代码,而这些代码很难理解。当他们进行更改时,会发生意想不到的破坏,导致测试时间过长以及出现需要修复的缺陷。
而专注于提高内部质量就是为了减少生产率的下降。事实上,有些产品会产生相反的效果,开发人员可以通过利用先前的工作轻松构建新的功能,从而加快开发速度。但这种令人愉悦的情况很罕见,因为它需要一支技术娴熟、训练有素的团队才能实现这一目标。但我们偶尔也会看到这一情况。
这里的微妙之处在于,有一段时期,内部质量较低的产品比内部质量较高的产品的生产力更高。在此期间,质量和成本之间存在某种权衡。当然,问题是,在两条曲线发生交叉之前,这段时间有多长?
在这一点上,我们遇到一个问题,为什么这是一个伪图。我们没有办法去衡量软件团队交付的功能。由于无法衡量产出,因而也就无法衡量生产率,因此无法对内部质量较低造成的后果(这也很难衡量)给出确切的数字。无法对产出进行衡量在专业工作中相当普遍:我们该如何衡量律师或医生的生产力?
我评估曲线交叉的方法是征求我所知道的熟练开发人员的意见。然而,答案让很多人感到惊讶。开发人员发现,质量很差的代码在几周内就会出现显著降低开发速度的现象。所以,在内部质量和成本之间能够进行权衡的地方并没有多少。即使是很小的软件开发工作也会从对良好的软件实践的关注中受益,当然,这是从我经验中所证明的这一点。
许多非开发人员倾向于,认为只有当开发团队粗心大意并出错时才会发生这种事情。但实际上,即使是最优秀的团队也会在工作时不可避免地制造出“Cruft”。
我喜欢用一则我和最好的技术团队领导聊天的故事来说明这一点。他刚刚完成了一个被普遍认为是非常成功的项目。客户对教辅的系统感到非常满意,无论是功能方面,还是构建时间和成本方面。我们的员工对这个项目的工作经验持肯定态度。技术主管也非常高兴,但也承认系统的架构并不是太好。我的反应是“这怎么可能?你可是我们最好的架构师之一啊!”,他的回答是任何有经验的软件架构师都很熟悉的:“我们做出了很好的决策,但直到现在才明白应该如何进行构建”。
许多人,包括软件行业的一些人,将构建软件比作建造大教堂或摩天大楼,毕竟,我们为什么要用“架构师”来称呼高级程序员呢?但构建软件存在于充满不确定性的世界,而这一世界不为物理世界所知。软件的客户只对产品需要什么功能有一个粗略的概念,并随着软件的构建了解更多的信息:特别是早期版本发布给他们的用户时。软件发开的构件:语言、库和平台,每隔几年就会发生重大的变化。在物理世界中,类似的情况是,一旦一半的建筑物建成并被占用,客户通常会添加新的楼层,并改变楼层平面图,而混凝土的基本性能每隔一年就会发生变化。
鉴于存在这种程度的变化,软件项目总是创造一些新颖的东西。我们几乎从未发现自己在处理一个以前已经解决过的、大家都能理解的问题,当我们构建解决方案的过程中,我们对这个问题了解得最多,因此,我常常听到团队只有在花了一年左右的时间去构建软件之后,才能真正理解软件的架构应该是什么。即使是最好的团队,他们的软件也会出现“Cruft”。
不同之处在,最好的团队创造出来的“Cruft”,但也消除了足够多的“Cruft”,这样,他们就可以继续快速添加功能。他们花费时间创建自动化测试,以便能够快速发现问题,并花更少的时间来消除 Bug。他们经常进行重构,这样他们就可以在 Cruft 积累到足以碍手碍脚之前就清除掉。由于团队成员是在不同目的上的工作,持续集成可以最大限度地减少 Cruft 的出现。一种常见的比喻是,这就好比清理厨房的台面和设备。要知道,在你做饭的时候,你不可能不把东西弄脏,对吧。但如果你不赶快把东西清理干净的话,那脏东西就会变干,这样就很难清理,所有的张东西都会妨碍你烹制下一道菜。
Dora 对精英团队的研究表明,质量和速度之间的选择,并非软件开发中唯一具有至关意义的选择,但这却是错误的。还有一种强烈的观点认为,在快速开发(如系统的频繁更新)和在生产中不会中断的可靠系统之间,存在双模选择(Bi Modal)。其实这是一个错误的选择,这点在《State of Dev Ops Repot》中严谨的科学研究中得到了证实。
几年来,他们一直使用调差的统计分析来梳理高效软件团队的实践。他们的工作表明,精英软件团队每天多次更新产品代码,在不到一个小时的时间内,即可完成将代码从开发状态更改为生产状态。当他们这样做时,他们更改失败率明显低于低效团队,因此他们从错误中恢复的速度要快得多。此外,这些精英软件交付组织与更高的组织绩效相关。
综上所述:
遗憾的是,软件开发人员通常并不能很好地解释这种情况。我曾无数次与开发团队交谈过,他们说,“他们(管理层)不会让我们编写出高质量的代码,因为这需要花费太多的时间。”开发人员通常需要通过适当的专业性来证明对质量的关注是合理的。但是,这种道德主义的观点暗示这种质量是以牺牲他们的观点为代价的——注定他们的观点会失败。令人讨厌的是,由此产生的粗制滥造的代码不仅让开发人员的日子更难过,也给客户带来了金钱上的损失。在考虑内部质量时,我强调我们只应该讲它作为一个经济论点来看待。较高的内部质量降低了未来功能的开发成本,这意味着花时间编写优秀的代码实际上就是降低了成本。
这就是为什么本文开头提到的问题没有抓住这一点的原因。较高内部质量的软件的“成本”实际上是负的。通常,成本和质量之间的权衡,我们在生活中的大多数决策中都已经习惯了这种权衡,但对于软件的内部质量来说,这种权衡是没有意义的。(它可以提高外部质量,比如精心设计的用户体验。)因为成本与内部质量之间的关系,是一种不寻常的、违反直觉的关系,因此通常很难理解。但是,如果能够理解它的话,对于以最高效率开发软件而言至关重要。