本文是一篇读书笔记。Martin大神的文章总是简单有趣又发人深省,本文翻译自他的《Is High Quality Software Worth the Cost?》,结尾是我个人的一些思考。
Is High Quality Software Worth the Cost
在软件项目开发过程中,一个常见的争论是,应该把时间花在提升软件质量,还是专注于发布更多有价值的功能。通常,交付功能的压力在讨论中占据了主导地位,导致许多开发人员抱怨他们没有时间研究架构和代码质量。
贝特里奇标题定律认为“Any headline that ends in a question mark can be answered by the word no”(任何以问号结尾的问题都可以回答no)。了解我的人知道我要颠覆这样的定律,但这篇文章会更进一步——我要颠覆问题本身。“我们值得为软件的高质量投入成本吗?”,这个问题假设了质量和成本之间是可以权衡的,这篇文章我将解释这种权衡的考虑是不适用于软件领域的,事实上高质量的软件生产成本更低。
尽管我的大部分文章都是针对专业软件开发人员的,但在本文中,我不会假设您对软件开发的机制有任何了解。我希望这是一篇对任何参与思考软件工作的人都有价值的文章,包括作为软件开发团队用户的业务领导们。
我们习惯于在质量和成本之间进行权衡
正如我在开篇中提到的,我们都习惯于在质量和成本之间进行权衡。当我更换智能手机时,我可以选择处理器更快、屏幕更好、内存更大、价格更贵的机型,或者我可以放弃一些品质来支付更少的钱。但这不是绝对的规则,有时候我们通过讨价还价也可以用便宜的价格买到高质量的商品;更多时候,我们对质量有不同的价值观,有些人并不在意一个屏幕比另一个更好。但这个假设在大多数情况下都是正确的:更高的质量通常意味着更高的成本。
软件质量包含很多方面
如果我要谈论软件的质量,我需要先解释它是什么。软件的质量是复杂的,它包含很多方面。我可以考虑用户界面:是否可以轻松引导我完成我需要完成的任务,让我觉得很高效而不至于失望?我也可以考虑它的可靠性:是否包含错误或缺陷?另一个方面是它的架构:是否将源代码划分为清晰的模块,以便程序员可以轻松找到并理解他们本周的工作要写哪块的代码?
这三个质量的示例并没有详尽所有,但足以说明一个重要点,如果我是该软件的客户或用户,某些质量相关的内容我是不会在意的。用户可以判断界面是否良好,一位高管可以判断该软件是否使他的员工的工作更高效,用户和客户会注意到缺陷,尤其是数据被损坏或系统暂时无法运行的时候。但是,客户和用户无法感知软件的架构是什么样的。
因此,我将软件质量属性分为外部 (如 UI 和缺陷)和内部(架构)。这两者的区别在于用户和客户可以看到软件产品是否具有高的外部质量,但无法区分内部质量。
乍一看,内部质量对客户来说并不重要
由于内部质量客户或用户看不到的东西——那它重要吗?让我们想象一下,Rebecca 和我编写了一个App来跟踪和预测航班延误。我们的两个应用程序都执行相同的基本功能,都具有同样优雅的用户界面,并且几乎没有任何缺陷。唯一不同的是,她的内部源代码组织得井井有条,而我的则是一团糟。还有一个区别:我的App卖 6 美元,她的卖 10 美元。
既然客户永远不会看到App的源代码,而且它不会影响应用程序的运行,那为什么会有人为 Rebecca 的软件额外支付 4 美元呢?更宽泛地来讲,为更高的内部质量付出更多的钱是不值得的。
从另一个角度来讲,用成本换取外部质量是有意义的,但换取内部质量是没有意义的。用户可以判断他们是否愿意支付更多费用来获得更好的用户界面,因为他们可以评估用户界面是否足够好、值花更多的钱;但是用户看不到软件内部的模块化结构,更能判断它是不是更好,为什么要为付钱呢?既然如此,为什么任何软件开发人员都应该投入时间和精力来提高工作的内部质量呢?
内部质量使软件优化变得更容易
为什么软件开发人员会关注内部质量问题呢?程序员的大部分时间都在修改代码。即使在一个新系统中,几乎所有的编程都是基于现有的代码完成的。当我想为软件添加新功能时,我的首要任务是弄清楚该功能如何兼容现有应用程序的流程。然后,我需要更改这个流程以适应我的功能。我也经常需要用到应用程序中已经存在的数据,因此我需要了解这些数据代表什么,梳理与其他相关数据的关系,以及新功能要增加哪些数据。
以上这些都与我对现有代码的理解有关,但是“软件很难理解”是特别容易的事情:逻辑可能复杂,数据可能难以理解,六个月前的一些参数名称可能Tony是知道的,但对我来说就像他离开公司的原因一样神秘。这些都是开发人员制造cruft的体现——当前代码与理想状态之间的差异。
cruft 常用来比喻技术债务。添加功能的额外成本就像支付利息一样,清理垃圾就像偿还本金。这是一个有所帮助的比喻,它确实鼓励许多人相信 cruft 应该能比在目前的实践中更容易测量和控制。
内部质量的主要特征之一是让我更容易弄清楚应用程序是如何工作的,这样我才知道怎样增加功能。如果软件很好地划分为单独的模块,我不必阅读所有 500,000 行代码,就可以在几个模块中快速找到几百行代码。如果我们更清晰地命名,就可以快速理解代码的各个部分的作用,而不必对细节感到困惑。如果数据合理地遵循基础业务的编程语言和结构,就可以很容易地理解它与客户服务代表的诉求之间的关系。Cruft 增加了我对“如何做出改变”的理解时间,也增加了我犯错的机会。如果我发现了问题,我会浪费更多的时间在了解问题和解决问题上;如果我没有发现问题,那么我们就会出现线上Bug,以后还会花更多时间来修复问题。
我的改动也会影响以后。我可能知道一种快速添加此功能的方法,但这是一条与程序的模块化结构背道而驰的路线,Cruft又增加了。如果我这样做,今天我能更快,但在未来几周和几个月内会让其他必须处理这部分代码的人变慢。一旦团队的其他成员都这样做,一个本来易于迭代的应用就会迅速积累Cruft,以至于每一个小改动都需要耗费数周的时间。
客户确实在意新功能的快速出现
至此,我们找到了为什么内部质量对用户和客户也很重要。更好的内部质量使得添加新功能更容易,因此更快更便宜。Rebecca 和我现在可能拥有相同的App,但在接下来的几个月里,Rebecca 的高内部质量让她每周都可以添加新功能,而我却受困于从一堆Cruft里刨出一个新功能。我无法追上 Rebecca 的速度,而且很快她的软件就比我的功能更丰富。然后我的所有客户都删除了我的App,转而使用 Rebecca的,甚至她还能提高自己App的价格。
可视化内部质量的影响
内部质量的根本作用是降低未来改动的成本。但是编写好的软件需要一些额外的努力,这在短期内确实会产生一些成本。
一种可视化的方法是使用下面这种pseudo-graph,其中我绘制了软件累积的功能与时间(以及成本)的关系。对于大多数软件工作,曲线是这样的。
这是内部质量差的情况。最初进展很快,但随着时间的推移,添加新功能变得越来越困难。即使是很小的更改也需要程序员理解大量的代码,并且是难以理解的代码。当他们进行变更时,会发生无法预期的风险,导致过长的测试时间和需要耗时修复的Bug。
注重高内部质量是为了阻止生产力的下降。事实上,在一些产品中能看到这样的情况,开发人员可以利用之前的工作轻松构建新的功能,这使得开发过程更快。像这样幸福的场景很少见,因为它需要一个有成熟技术和规范的团队。但我们确实偶尔是能见到的。
这里的微妙之处在于,最开始的一段时间内部质量低比内部质量高更有效率。在此期间,质量和成本之间存在某种权衡。然而问题是:这条线交叉之前的时间有多长?
这里我们就知道了,为什么这是一个pseudo-graph,软件团队的生产力是无法衡量的。由于无法衡量产出,因此无法衡量生产力,因此也无法对内部质量低下的后果(这也难以衡量)给出确切的指标。无法衡量产出在专业性的工作中是很常见的——我们如何衡量律师或医生的生产力呢?
我评估界线交叉点的方法是征求我认识的资深开发的意见,答案让很多人感到惊讶。开发人员发现质量差的代码会在几周内就会显著减慢他们的速度。因此,内部质量和成本之间的权衡适用的范围并不大。我的经验告诉我,即使是很小的软件也会受益于良好软件实践。
即使是最好的团队也会创造Cruft
许多非开发人员倾向于认为cruft只有在开发团队粗心并犯错时才会发生,但即使是最优秀的团队,在工作中也不可避免地会产生一些cruft。
我喜欢用一个我与我们最好的技术团队负责人聊天的故事来说明这一点。他刚刚完成了一个被大家认为非常成功的项目。客户对交付的系统感到满意,无论是在功能方面,还是在交付时间和成本方面,我们的员工对该项目积累的经验也持积极态度。这个技术负责人总体上是高兴的,但他承认系统的架构并不是那么好。我的反应是“怎么可能,你是我们最好的架构师之一?” 任何有经验的软件架构师都会对他的回答感到熟悉:“我们做出了正确的决定,但直到现在我们才明白我们应该如何构建它”。
很多人,包括软件行业的不少人,都将构建软件比作建造大教堂或摩天大楼——毕竟我们对高级程序员使用“architect”这个称呼。但是构建软件存在于物理世界未知的不确定性世界中。软件的客户一开始对他们需要的产品功能只有粗略的概念,随着软件不断迭代才越来越清楚 —— 尤其是在向用户发布早期版本时。建构起软件开发的部分——编程语言、库、平台——每隔几年就会发生重大变化。对标物理世界来说,客户在建筑物建成一半时开始添加新楼层并更改平面图,而混凝土的基本特性每隔一年就会发生变化。
Dora对精英团队的研究
质量和速度之间的选择并不是软件开发中唯一具有直觉意义的选择,而且是错误的。还有一种强烈的观点认为,在快速开发、频繁更新系统和不会中断生产的可靠系统之间进行双模式选择。State Of Dev Ops Report中仔细的科学工作证明了这是一个错误的选择。
几年来,他们一直使用调查的统计分析来梳理高绩效软件团队的实践。他们的工作表明,精英软件团队每天多次更新生产代码,在不到一个小时的时间内将代码更改从开发推送到生产。当他们这样做时,他们的变更失败率明显低于速度较慢的组织,因此他们可以更快地从错误中恢复。此外,这种精英软件交付组织与更高的组织绩效相关。
鉴于这种程度的变化,软件项目总是在创造一些新奇的东西。我们很少处理那些以前已经解决过的容易理解的问题。自然,我们在寻找解决方案时对问题的了解最深刻,所以我经常听到团队只有在他们花了一年左右的时间构建软件之后才真正最了解他们的软件架构应该是什么。即使是最好的团队也会在他们的软件中遇到cruft。
不同之处在于,好的团队既创造了更少的cruft,又消除了足够多的cruft,以便他们可以继续快速地迭代。他们花时间创建自动化测试,以便快速发现问题并花更少的时间解决问题。他们经常重构,以便可以在cruft堆积到成为阻碍之前将其移除。持续集成最大程度地减少了由于团队成员因交叉目标而造成的cruft。一个常见的比喻是,它就像清理厨房里的工作台面和设备。做饭的时候不能不弄脏东西,但是如果不快速清理,脏东西就会变干变得更难清理,所有脏东西都会妨碍下一道菜的烹饪。
高质量的软件生产成本更低
总结来说:
- 忽视内部质量导致快速积累cruft
- cruft会减慢新功能的开发速度
- 即使是一个优秀的团队也会产生cruft,但保持高的内部质量可以控制它
- 高的内部质量将cruft降至最低,允许团队以更少的精力、时间和成本迭代
可悲的是,软件开发人员通常不能很好地解释这个。无数次我与开发团队交谈时,他们说“他们(管理层)不会让我们编写高质量的代码,因为需要太长时间”。开发人员只在需要证明专业精神时才承认关注质量的是合理的。这意味着追求质量是有代价的。令人讨厌的是,由此产生的粗劣代码既让开发人员过得更艰难,又让客户付出了代价。在考虑内部质量时,我要强调我们只是因为考虑到经济原因。高的内部质量能降低未来功能的成本,这意味着花时间写好的代码实际上是降低成本的。
这就是为什么本文开头的问题没有抓住重点。高质量软件的“成本”是负数。我们习惯于在成本和质量之间进行权衡,这是我们生活中的权衡习惯,但对于软件的内部质量来说是没有意义的。(它适用于外部质量,例如精心设计的用户体验。)由于成本和内部质量之间的关系是一种不寻常且违反直觉的关系,因此通常很难被理解,但是理解它正是最大化软件开发效率的重中之重。
我的思考
1. “高质量和快速交付之间如何选择”问题本身可能是不成立的,因为追求内部质量的成本其实是负数,高的内部质量反而能提升生产力。
直觉上来说,想要快速交付与追求高质量是相悖的。作为一名测试,在需要压缩测试进度的时候,我也常常跟PM这样battle:“不加班的话,要么砍砍需求,要么缩减测试范围(牺牲部分功能的质量)”。
我们习惯性地用这样的思维理解开发和测试对质量的追求:
开发/测试追求高质量 = 额外的开发/测试人力 + 额外的开发/测试时间 = 保障质量耗费更多成本 = 产品发布慢 = 赚不到钱
然而事实也许不是这样,首先项目的发布速度和质量不是开发或测试某一个角色决定的,一个项目从启动到上线就至少包含了需求/设计、开发、测试、运维;其次单次功能迭代是否能快速上线并不能决定这个产品是否能在季度末拿到一个正向的盈利,而是取决于该项目的整个产研团队是否能保持持续的生产力,构建一个“又快又好”的协作系统,足以跟得上业务发展的需求。
我们的思路应该是这样的:
需求合理 + 代码质量高 + 测试效率高 + 运维可靠 = 产品发布频率符合预期 + 内部质量高+外部质量高 = 保持良好的持续生产力 = 赚钱
需求不合理 + 代码积累cruft + 测试随意点点 + 运维差劲 = 产品发布快 + 内部质量差 + 外部质量看似高 = 持续生产力逐步衰减(文章中甚至认为在几周内就会出现转折,我对这个时间范围持保留意见) = 亏钱
2. 内部质量是什么,测试角色如何保障产品内部质量?
文中认为内部质量包含了项目代码的内部架构。好的内部质量应该方便开发人员不断高效引入新代码,且能支撑复杂的现实场景下的持续开发(易于重构、易于修正已有的cruft、易于兼容需求的变化)。
看起来我们离提升内部质量就差一个更优秀的开发团队,但等一等,引入新代码或重构后如何保障旧功能没有损坏?好的内部质量还需要一套稳定高效的CI/CD机制来保障。
测试作为“Quality Assurance”的角色,表面上看起来更多的是保障产品的外部质量,如核心功能没有缺陷、用户体验良好、系统不会崩溃或超时等。但同时也应该关注内部质量的评估和保障,我们可以为这些事情努力:
- 为单个项目构建合理的CI流程,保障代码基础质量(如静态检查、单元测试、编译检查、启动检查、自动测试等)—— 持续集成
- 支持快速、频繁地进行多服务的集成测试,包括功能、性能、兼容性等 —— 测试自动化
- 支持频繁发布到生产环境,在真实用户场景中快速得到质量反馈 —— 持续部署 & 生产QA
- 人人都能看到测试方法、测试结果,并能提前且轻易地评估质量好坏和风险;人人都能看到产品线上的各项质量指标、用户反馈等,并能轻易地定位问题 —— 质量的可观察性
- 测试用例/代码本身可维护、可自检、内部质量高
3. 追求内部质量这件事在资源紧张的现实中真的适用吗?
“但是,我们的项目真的没有资源保障内部质量了,甚至有时候仅仅保障外部质量都紧张得不行”
“这个产品还不知道能不能活下去,我就是想要快速推出去验证市场反馈,再快速调整方向”
亚马逊的“逆向工作法法”告诉我们,不应该有了工具和技术然后去寻找它们的适用场景或优化方案,而是要先研究用户的需求,再寻找契合的解决或创新方案。我们最终是为业务的发展、用户的需求负责,如果业务或用户对我们的要求并不是持续的生产力,而是短期的生产力以供快速试错或对用户体验有容忍度,那就应该调整质量保障的重点和手段。比如相比花时间进行异常场景的测试更侧重生产环境的QA,保障用户反馈和质量风险的快速暴露和恢复。
在这样的情况下,我们免不了在低内部质量导致的生产力衰减节点前后挣扎。根据我自己的经验,当下被标记为待后续版本优化的细节,大概率在以后也不会出现在排期表上;积攒的技术债务只会在某个“非优化不可”的节点才被重新刨出土来;积攒的没有做自动化的测试也会在某次项目大规模重构时被迫从头加上。这是“现实主义”的解法,我们得接受它的存在。
从这个角度来说,我们还是免不了做权衡的。这个问题很难,我还在努力探索中。