什么是技术债务?##
许多团队都受技术债务困扰,不过,很少有团队能真正地设计一个计划从中挣脱出来。为了更好的理解如何才能摆脱债务,我们首先要正确地理解什么是技术债务。
技术债务是由团队为了短期的项目利益故意做了欠佳的技术决策而招致的。例如,为了使一个产品更快的投放市场,团队可能不会像面对一段棘手的代码那样,编写深入的自动化测试。或者,他们可能会决定基于一个很快就会过时的框架构建项目,而不是花钱购买那个框架的一个经过升级、服务支持更好的版本。不管决策是什么,关键是要认识到,真正的技术债务是团队为了获得短期利益故意做了会招致长远债务的决策。
这意味着,许多我们通常归咎于“技术债务”的事情实际上根本就不是债务。例如,随着系统使用年限增加,团队会无法一直保持最佳编码实践,在那种情况下出现“位衰减(bit dot)”是正常的,不是债务。或者,团队自愿做出的技术决策在随后实现时竟然发现是不正确的,这也不是技术债务。在任何一种情况下,团队都不是为了获得短期利益而故意做了欠佳的长远决策。这种差别很重要,因为我们只有在究竟是什么组成了技术债务的问题上达成一致,我们才能针对如何减少技术债务做出最佳决策。
考虑我们在无意间引入代码库的债务所带来的影响也很重要。不过,由于这类债务不是有意承担的,所以我们无法计划如何提前解决它。在这些情况下,我们一定要在发现的时候优先考虑并解决这种债务……就像团队解决新发现的Bug所做的那样。
一个金融比喻##
我们经常将技术债务与金融债务进行对比。但是,这是一个对等的类比吗?比如,如果不首先确定何时以及如何偿还,银行真会愿意借给我们钱吗?很可能不愿意。既然不首先描述如何偿还债务,我们就确实无法借到金融资本,那么我们也就不应该以这种方式对待技术资本。
这个金融比喻确实非常有效,因此,我们将对它进行更充分的讨论。
- 偿还计划
正如金融债务一样,在谈论技术债务时,我们需要确定两件事:如何偿还债务和何时偿还债务。
- 如何偿还债务
让我们以前文提到的放弃自动化测试为例。如果为了一项功能能够尽快投入生产环境,我们的团队自愿决定放弃为棘手的代码段编写自动化测试,那么我们需要确定我们将如何纠正那块债务。在这种情况下,我们只需要表明,我们会在将来的某个时点回到这段代码,然后添加测试。不过,只表明“我们将在稍后添加测试”并没有合理地解决这个问题。为了帮助干系人真正地了解我们的决策对招致技术债务会有多大的影响,我们需要尽我们最大的能力准确地说明等到后来添加自动化测试的影响。比如,我们应该指明需要添加测试的代码段。我们也需要明确指出,在已有的代码段上增加自动测试比在代码段创建时直接添加测试总是难度更大成本更高。最后这点很重要,因为它明确指出,将这项工作推迟到后来完成,我们实际上要比现在直接完成花更多的钱来做这项工作。换句话说,……债务是会产生利息的。
在讨论完偿还方式之后,团队及其干系人可能会意识到,这个问题要比他们是否应该在编写代码时编写自动化测试更复杂。实际上,他们可能会发现,在两个极端之间,他们确实有多种更适合整个项目的选择。比如,他们可能会决定,使用类似“圈复杂度(Cyclomatic Complexity)”这样的指标识别出新特性中最复杂的部分是非常有意义的,并立即添加测试覆盖那些方面,而把简单一些的方面推迟。或者,他们可能会决定,像单元测试这种更简单但同时价值也更低的自动化测试类型可以立即添加,但像自动化用户验收测试这种难度更高的自动化测试可以推迟到将来的冲刺中。不管是哪种决定,团队及其干系人都不大可能做出,因为他们没有在第一时间对技术债务进行讨论。
- 何时偿还债务
除了指明如何偿还债务外,我们还希望指明何时偿还债务。通常,债务越早偿还越好。出于这个原因,最好是在债务产生的时候安排债务偿还工作,以便更好地传达承担债务的影响。比如,如果你的团队遵照一个由冲刺构成的时间表,那么你可能会选择将工作安排到下一个冲刺,或者,在最坏的情况下也不超过几个未来的冲刺。
通常,团队都会有最美好的愿望,就是在债务出现的时候偿还,但他们只是似乎从来都没有时间那样做。当你要消除债务的时候,在日历上记下最后偿还期限,并坚持按时完成。
- 如果违约会怎么样 ?
对于我们的金融比喻,最后一部分是弄清楚,如果我们选择拖欠债务,会出现什么后果。在金融领域,如果我从来不偿还汽车贷款,那银行会开走我的汽车。拖欠技术债务的后果同样应该明确指出。
让我们继续自动化测试的例子,如果团队决定不偿还技术债务,那么以现有的未经测试的功能为基础构建新功能只会使难度越来越大成本越来越高。在生产环境中,我们可能会看到更多的Bug报告,这意味着,客户可能会对我们的产品质量产生不好的印象。我们快速响应市场变化的能力也会被削弱,因为我们产品的很大一部分要么很难快速修改,要么快速修改风险太大。
上述各点都需要向干系人讲清楚,帮助我们避开技术债务违约的诱惑。
- 应对位衰减
截至目前,我们讨论的所有内容都集中在有计划的技术债务上,就是那些作为正常项目的一部分团队故意招致的债务,而如果我们不讨论未计划的技术债务,我们就失职了,因为它们也困扰着许多项目:位衰减。
如前所述,位衰减不是我们故意招致的,我们无法提前决定如何以及何时偿还,在这个意义上,它不同于正常的技术债务。不过,虽然我们无法确切地知道位衰减在我们项目中的呈现形式,但这并不意味着我们无法计划它。
就像我们计划偿还已知技术债务时所做的那样,我们也可以在我们的项目计划中加入一个缓冲区,用于在每次冲刺中处理位衰减。虽然那时可能并不知道填充这个缓冲区的具体任务,但有一个这样的缓冲区在那里,使我们有了一个专门的空间,可以应对那些未计划的问题,如Bug,必须立即处理的小规模重构,或者我们称之为代码库自然老化和衰减的小块系统维护。
但是,对于那些用几个小时的开发时间都无法解决的更大的问题,该怎么办?可能会有更多让我们倍感困扰的系统性问题,如基础设施故障或架构日益老化无法再满足业务需求。由于这些问题太大,不容易解决,我们就可以使用这个缓冲区确认和研究这些问题,那样,我们后续就可以在项目中给予它们应有的注意。比如,在基础设施故障的情况下,或许我们能够确定基础设施中最经常出现故障的部分,那样我们就可以在待办事项列表中增加故事,用于替换这些部分。然后,我们会在正常的开发中排定优先级并照此调度那些故事。或者,在架构日益老化的情况下,我们可以花些时间来确定,我们对于架构能够动态做出的最有价值的修改是什么,然后将那些修改分解成可以在将来的冲刺中分段处理的故事。不管问题是什么,有时间确定问题并制定计划就意味着可以像解决其它任何技术问题一样解决它。
- 预算
既然我们已经对招致技术债务的影响有了一个更好的了解,那么我们该如何真正确保偿还技术债务?为了对此有一个感官的认识,我们将上述金融比喻向另一个方向扩展……预算。
我们中有许多人每隔几周就会挣得一份固定工资。这份工资是年度工资的一部分,全年平均分布。比如,如果我们每年能挣52000美元,那么我们可能每隔2周就会收到2000美元……就是说,每年26次。
不过,虽然按规则我们每次都能领到2000美元工资,但我们实际上不大可能全部存放在活期账户上。相反,我们更可能只将一部分存放在活期账户上,而把其它部分用于长期投资或者偿还债务。
比如,我们起初有2000美元存款,预算分解实际上可能是下面的样子:
500美元用于长期投资,比如存入退休账户
200美元用于偿还短期债务,如信用卡
300美元用于偿还长期债务,如按揭贷款或汽车贷款
250美元直接扣除,用于支付地方或国家税收
剩下的750美元存入活期账户
现在,虽然没人对这种安排感到特别兴奋,但也基本没人质疑。在某种程度上,它简直是公认的规范。既然这种安排被如此广泛地接受,那么为什么不将它扩展到我们的项目计划中呢?
想象一下,我们正以故事点为单位计划一次冲刺,并且,我们为那个冲刺分配了50个故事点。尽管我们希望将所有50个点都用在新的开发上,但实际上,最合理的方式是安排一些点用于偿还技术债务。比如,对于原有的50个点,我们可能决定安排:
10个点用于偿还技术债务
5个点用于不间断的代码库维护,如修复Bug或处理位衰减
35个点用于新的开发
看看我们的点预算,我们可以看到它与我们前面描述的金融预算有一些明显的相似之处。虽然每个冲刺我们有50个点用来做事,但并不是所有那些点都用在新的开发上。更确切地说,我们必须把那些点中的10个用于偿还我们有意招致的技术债务,正如我们把工资的一部分用于偿还我们的长期和短期债务。此外,我们将5个点完全用于保持不间断的代码库维护,正如我们把每份工资的一部分用于税收,帮助维持我们社区的正常运转。剩下的点是我们自己的,可以根据我们的意愿用于任何新的开发,正如剩下的钱存入活期账户,我们可以根据自己的意愿自由支配。
通过工作预算的方式,我们可以确保我们的项目仍然在向前进行,而不允许它在技术债务的重压之下崩溃。
当然,就像个人预算一样,这种预算会有些偏差。比如,如果我们偿还技术债务的速度比预期快,那么我们就会有些剩余的故事点可以用在新功能开发上。而同样的,如果我们发现我们承担着特别多的技术债务,那么我们可能需要稍微降低新功能的开发速度,直到技术债务的数量回到一个更可控的水平。
小结##
通过以金融债务做类比考虑技术债务,我们可以对什么时候会招致技术债务有一个更明智的判断,有利于项目改善。在项目进行过程中,我们还可以更好地计划试图偿还债务所带来的影响。
而且,通过在日常工作中计划日常预算,我们可以更好地了解我们的债务什么时候是可控的以及什么时候会让我们负担过重。