4.5 技术债务测量
概述与动机
技术债务,一个由Ward Cunningham创造的比喻,描述的是过去权宜之计所积累下来的成本,那些让某样东西更早发布出去、却让代码库此后更难变更的权宜决策,其方式与财务债务让你现在花钱、代价是以后支付利息如出一辙。每个代码库都带有一些技术债务,这本身并不自动等于失败;这个比喻真正的价值在于,它把债务定位为一种可管理的权衡,而不是一个可耻的秘密,也不是一种不可避免的、永久的负担。本主题讲的是通过测量,让这种权衡变得可见、可管理,而不是任由它停留在一种模糊的、被持续降低优先级的担忧之中,每个工程师都能感觉到它,却没有人能拿着证据据此采取行动。
在本主题之前的那些主题,复杂度(4.1)、覆盖率(4.2)、变动量与热点(4.3),以及静态分析(4.4),每个主题都揭示了技术债务的一个方面。本主题的工作是综合:把这些各自独立的信号,加上那些从不会出现在任何自动扫描中的条目(一个未被记录的架构性权宜之计,一次被刻意推迟的迁移),转化为一份单一的、有优先级的、可见的积压清单,让它能与功能工作公平地竞争投资,而不是仅仅因为它没有附带任何指标、在规划会议上也没有拥护者,就默认输掉这场竞争。
对大团队而言,未被管理的技术债务以一种真正危险、也容易被低估的方式累积:每一个新的权宜之计,都会让下一次变更稍微变难一点,这又造成了更多权宜之计的压力,进而进一步累积。维护系统长达多年的企业和政府组织,尤其容易受到这种累积效应的影响,而本主题的核心建议,一份可见的、量化的、有优先级的债务积压清单,正是让一个组织能够真正刻意地管理这种权衡、而不是漂流进危机的机制。
核心原则
- 技术债务是一个刻意选用的比喻,用来描述一种可管理的权衡,而不是一个可耻的秘密。 一些明知故犯地承担的债务,是一项合理的商业决策。
- 未被测量的债务,默认会在与功能工作的优先排序竞争中败下阵来, 不是因为它不重要,而是因为它没有可见的拥护者。
- 用决策者能够权衡的说法来量化债务:修复成本与背负成本相比。 一句模糊的”这段代码很乱”的说法,很少能在竞争中比得过一个具体的功能请求。
- 债务会累积。 每一个新的权宜之计,都会让未来的变更变得稍微更难一点,而这种效应如果不加管理会加速。
- 并非所有债务都应该被偿还。 如果修复它的成本超过了背负它的成本,一些债务值得被无限期地背负下去。
建议
建立一份单一、可见的技术债务积压清单
把这一部分前面各主题的信号,复杂度离群值、低变异杀死率的区域、热点、未解决的静态分析发现,与只有人才能识别出来的债务条目(一个架构性权宜之计、一次被推迟的依赖升级、一个未被记录的临时解决方案)整合到一份可见的积压清单中,用与你的功能积压清单同等的严谨和可见度来跟踪它。只存在于个别工程师记忆中或散落在代码注释里的债务,从优先排序的角度来看,实际上等于不存在。
量化每一项债务的成本及其背负成本
对每一项债务,估算两个数字:修复它的成本(工程时间、修复本身的风险)和不修复而背负它的成本(相关工作因此变慢多少,它承载了多少额外的缺陷风险,它阻碍了多少其他工作)。这种框架直接借用了财务债务比喻自身的逻辑,让决策者拥有一个真正的基础,可以与功能工作的成本和预期价值进行比较,而不是一句抽象的、未经量化的抱怨。
用影响力来确定优先级,而不是用存在时长或拥护声音的大小
按背负成本与受影响代码被触碰的频率(主题 4.3 的变动量数据在这里直接有用)的结合,对债务条目进行排序:一个位于代码库中很少被修改角落的条目,无论多么令人不快,都远不如一个直接坐落在你最活跃开发路径上的条目重要。抵制按哪个条目在积压清单上停留最久、或哪位工程师最持续地为它辩护来确定优先级的做法,这两者都无法可靠地与真实的业务影响相关联。
为债务补救分配专门的、受保护的产能
一份必须在每个规划周期中逐项与每一个新进功能请求竞争的债务积压清单,往往一贯地输掉,因为功能工作通常拥有一个更清晰、更直接的业务拥护者。为债务补救专门分配一个受保护的工程产能百分比,一种常见的模式是10%到20%之间,提前决定,而不是每个迭代重新协商,这样债务偿还就会作为常规工作的一部分发生,而不是只在一场危机的余波中才发生。
明确地接受一些债务作为永久性的,并这样明说出来
不是每一项条目都属于一份活跃的补救计划。在修复成本真正超过无限期背负一项条目的成本的地方,尤其是对一个稳定、很少被触碰、即将被淘汰的系统中的代码而言,明确地记录这个决定,把这个条目移到一个被刻意降低优先级的类别,而不是任由它无限期地停留在一份活跃的积压清单上,它的持续存在会悄悄暗示着一项实际上永远不会真正发生的工作。
权衡取舍:利与弊
| 方案 | 优点 | 缺点 |
|---|---|---|
| 没有正式的债务跟踪 | 没有开销 | 债务默认会在优先排序竞争中败下阵来;不可见地累积 |
| 非正式、临时的债务意识 | 开销低,具备一定可见度 | 不一致;依赖个人记忆和个人拥护 |
| 正式的、量化的债务积压清单 | 公平地竞争投资;使有依据的权衡成为可能 | 需要持续的维护和量化纪律 |
| 受保护的、专门的补救产能 | 确保偿还持续发生,而不仅仅是被动反应 | 短期内减少了可用于功能工作的产能 |
核心张力是即时交付压力与长期可维护性之间的张力。功能工作几乎总是比债务补救拥有更清晰、更直接的业务拥护者,这就造成了一种结构性压力,让债务在每一次单独的优先排序决策中都败下阵来,即使它的累积成本很高。解决这种张力的办法,是通过受保护的、预先分配的产能,把债务补救完全从逐项竞争中移出来,这样这项权衡就会被刻意地、提前决定,而不是在每一个单独的规划周期中被重新争论,而且通常还会输掉。
与团队讨论的问题
我们是否拥有一份单一、可见的技术债务积压清单,还是债务意识主要存在于个别工程师的脑子里? 如果诚实的答案是后者,那这就是本主题建议首先弥补的单一最大缺口。
对我们最重要的债务条目,我们能否用足够具体的说法陈述它的修复成本和背负成本,以便与一个功能请求公平比较? 如果不能,用一个真实、当前的条目,一起把这种量化当作一次小组练习来实践。
我们的工程产能中,实际上有多大比例投向了债务补救,这个比例是刻意决定的,还是只是功能工作分配完之后碰巧剩下的? 看看你近期真实的迭代,计算出真实的数字,而不要依赖印象。
我们的债务积压清单是按真实的业务影响来排优先级的,还是按哪个条目被提出得最持续、或存在时间最久来排优先级的? 把你当前的优先排序与变动量数据(主题 4.3)交叉引证,看看两者是否一致。
哪些债务条目我们应该明确接受为永久性的,而不是任由它模糊地留在积压清单上? 找出至少一个修复成本真正超过背负成本的真实条目,讨论把它移到一个明确降低优先级的状态。
过去一年我们的债务积压清单发生了什么变化,增长、缩小,还是持平,这个趋势与我们的直觉相符吗? 随时间跟踪这一点,而不要只看某一个时刻的单一快照;趋势往往比任何给定时刻的绝对大小更具信息量。
行业视角
初创公司。 在这个阶段,刻意的、明智的债务往往是一种合理的策略:快速发布以验证一个假设,同时有一个清晰的计划,如果产品被证明可行就重新审视具体的权宜之计,这是一笔合理的交易,而不是一次失败。风险在于,随着代码库的成长,失去了对哪些权宜之计是刻意且可逆的、哪些已经悄悄变成永久性的、未被审视的负债的追踪。
小型企业。 一份简单的、共享的清单,哪怕只是非正式的,列出你已知的权宜之计及其大致的修复成本,在这个规模上通常已经足够。最值得养成的纪律,只是定期重新审视这份清单,而不是任由它悄悄累积、因为熟悉而变得不可见。
企业。 在这个规模上,受保护的、预先分配的补救产能最为重要,因为在没有结构性制衡的情况下,债务与功能工作之间逐项的优先排序竞争,会在数十个团队中同时可靠地偏向功能。在全组织范围内标准化债务量化的做法,这样债务条目才能在各团队之间被公平地比较,以支持组合层面的投资决策。
政府。 长寿命系统在数年或数十年、单独看都合理的渐进式需求变更中积累债务,往往在一场危机迫使问题浮现之前,根本没有任何正式的债务跟踪。一份量化的、可见的债务积压清单,是向监督机构论证现代化预算正当性的一个真正有说服力的工具,因为它把一句模糊的”系统老旧”的说法,转化成了一个具体的、有成本核算的投资论证。
案例
企业。 一家电信公司的计费平台,在超过十年的时间里积累了非正式承认、却从未被正式跟踪的技术债务,工程师们在回顾会议上常常提到”计费引擎一团糟”,却从未有任何后续跟进。一位新的工程总监要求每个团队建立一份量化的债务积压清单,为每一项估算修复成本和背负成本,并从此为债务补救分配固定的15%工程产能。一年之内,背负成本最高的五个条目,按数量算只占总积压清单的一小部分,已经被解决,计费相关部署的变更失败率(主题 2.10)也出现了可衡量的改善,证明了优先针对背负成本最高的条目、而不是按任意顺序处理积压清单所带来的不成比例的影响。
政府。 一家国家统计机构的核心数据处理系统,最初建于二十多年前,尽管员工普遍非正式地承认其中相当一部分很脆弱、不易理解,却从未进行过正式的债务评估。一次结构化的债务评估,结合静态分析的发现、热点数据,以及对为数不多的、理解最古老组件的在职工程师的访谈,产生了一份量化的、有优先级的积压清单,直接支持了一项多年期的现代化预算请求。关键的是,这项评估还明确识别出几个稳定、很少被触碰的遗留组件,认为把它们保持原样是合理的,从而避免了一次不必要地宽泛且昂贵的全系统重写,转而对数据显示背负持续成本最高的具体区域进行有针对性的投资。
商业理由:动机、投资回报与总拥有成本
刻意管理技术债务的回报,是避免了累积成本:每一个未被解决的权宜之计,都会让未来的变更稍微变难一点,如果不加干预,这种效应会加速,最终产生一个脆弱到连简单的变更都变得缓慢而危险的代码库。上面的电信案例具体展示了这种回报:针对少数背负成本最高的条目,就产生了可衡量的交付和质量改善,相对于这些条目在总积压清单中所占的一小部分而言,这种改善是不成比例的。
总拥有成本是分配给补救的受保护产能,通常是10%到20%的工程时间,这是一项真实、可见的成本,短期内会与功能开发速度相竞争。这份成本值得付出,因为替代方案,未被管理的、不断累积的债务,最终会以整个代码库中放缓的交付和升高的缺陷率的形式,付出远更高的代价,而不仅仅是那些未被解决的具体条目。
反模式与陷阱
- 没有可见的、被跟踪的债务积压清单: 债务默认会在优先排序竞争中败下阵来,不可见地累积。
- 模糊的、未经量化的债务主张: 在规划中很少能与具体的、量化的功能请求竞争过。
- 按存在时长或拥护声量而不是影响力来确定债务优先级: 把有限的补救产能引导到了错误的地方。
- 没有为补救分配受保护的产能: 债务偿还只在一场危机之后被动地发生,而不是作为一种常规的、刻意的做法。
- 把所有债务都当作同等值得修复: 在低影响条目上浪费精力,而高背负成本的条目却无人处理。
- 任由债务无限期地留在一份活跃的积压清单上,从不决定它是永久性的: 暗示着未来实际上永远不会发生的工作,也让真正的优先排序变得混乱。
成熟度模型
- 第一级,启动: 技术债务被非正式地讨论,没有被跟踪的积压清单,也没有量化;它一贯地在与功能工作的竞争中败下阵来。
- 第二级,发展: 一些团队非正式地跟踪债务,但没有一致的量化、跨团队可见度,或受保护的补救产能。
- 第三级,标准化: 全组织范围内存在一份可见的、量化的债务积压清单,受保护的补救产能被一贯地分配。
- 第四级,管理: 债务条目按测量出的影响力(背负成本结合变动量)确定优先级,被永久接受的债务被明确记录,而不是含糊不清。
- 第五级,协奏: 组织能够指出具体的、可衡量的交付或质量改善,这些改善可以追溯到有针对性的债务补救,债务管理是与功能工作并列的、工程投资决策中一项常规、可信赖的输入。
讨论思路
- 我们目前背负成本最高的单一债务条目是什么,我们能量化它吗?
- 今天我们实际有多大比例的产能投向了债务补救?
- 哪一项债务条目我们应该明确接受为永久性的,而不是模糊地留在积压清单上?
- 过去一年我们的债务积压清单增长了、缩小了,还是持平?
- 一次量化的债务评估会揭示出我们当前非正式的认知所遗漏的什么?
要点回顾
- 技术债务是一种可管理的权衡,而不是一个可耻的秘密;量化它,而不是任由它停留在一种模糊的、被持续降低优先级的担忧之中。
- 为每一项条目量化修复成本与背负成本,让它能与功能工作公平地竞争。
- 按影响力(背负成本结合变动量)来确定优先级, 而不是按存在时长或拥护声量。
- 分配受保护的、专门的补救产能,提前决定,因为否则债务在与功能工作逐项竞争时可靠地会败下阵来。
- 在修复成本超过背负成本的地方,明确接受一些债务作为永久性的, 而不是任由它模糊地留在一份活跃的积压清单上。
参考文献与延伸阅读
- Cunningham, Ward, “The WyCash Portfolio Management System” (OOPSLA experience report, 1992):技术债务比喻的起源。
- Managing Technical Debt: Reducing Friction in Software Development,Philippe Kruchten、Robert Nord、Ipek Ozkaya著(对技术债务测量与管理的全面探讨)。
- Refactoring: Improving the Design of Existing Code,Martin Fowler著(一份债务积压清单最终会用到的补救技术)。
- Your Code as a Crime Scene,Adam Tornhill著(把热点分析作为债务优先排序的一项输入,主题 4.3)。