我们曾遇到过最后期限即将到来、时间非常紧迫的情况。当时,我们必须尽快修复Bug,然而其中的一个Bug特别坚韧,任我们百般努力也无可奈何!随后,我的某个同事接手了调试工作。他强行写入了一些应该从数据库中检索来获取的值——它们在系统运营的最初几个月里不会发生变化——随后……系统神奇地正常工作了!
对于这类“莫名其妙的代码”,我的这位同事以非常风趣的埃及俚语称之为“Habya’a”,意即临时拼凑的组件。
我同事和他的创造性俚语相仿,Ward Cunningham在1992年把这种糟糕的代码称之为“技术债务”——在Wiki百科上对技术债务的定义是“在判定某项任务完成之前,需要先完成的工作”,而Steve McConnell将技术债务定义为“一种设计或构建的方法,它是一种短期内的权宜之计——因为它会产生这样的一种技术环境:与现在动手完成相比,稍后完成同样的工作需要更高的投入。”
如果从实用主义的角度来看待技术债务,我们会发现实际上它并不总是件坏事。当截止日期已过的时候,技术债务就相当于为了交付而付出的“高速公路的过路费”。我的另一个朋友曾经这样对我说“技术债务就像在没有停车区域的地方停车:乱停车是错误的行为,而且会导致我们吃罚单,但有时候我们为了赶上旁边建筑里的一次重要约会,就不得不这样铤而走险!”
所以,有时候效益成本比决定了一切!然而技术债务必须及早解决,它与像金融上的债务相似的另一个地方,正是在于它们都会产生利息。
这里的利息是指在每次维护系统的过程中,我们面对以下状况需要付出的努力:由于紧耦合、过大的类、未经测试的代码或任何其他形式的技术债务,而导致代码和/或设计的维护变得极其困难。
从我的观察来看,技术债务的总利息并不固定,而是会随着时间的推移而增长。我的意思是,在面对一个带有技术债务的系统时,每一个Sprint中我们都需要在系统维护上花费比之前更多的精力。这个现象源自以下两项因素:
维护时很有可能会引入额外的债务,这是因为当我们的系统中拥有一些混乱的代码时,任何维护都会遵从相同的代码和/或设计方法。这些新增的债务会在下一次维护时消耗更多的精力,而这一切将不断重复。
随着时间的流逝,由于没有遵从设计模式以及缺乏文档化,更多的开发者会从各自对代码或设计片段如何工作的假设出发,给系统打上不同的补丁。毫无疑问,这将在系统中引入新的Bug。而修订这些新Bug又会引入更多的Bug……
基于以上原因,每份利息都会对技术债务的增长“做出贡献”,因此这里的利息实际上是复利计算方式。复利计算使用以下指数公式:
Yt = Y0(1+r)t
在这里,Yt是在第t个Sprint时的债务值,Y0是债务初始值,r代表增长率,而t代表Sprint序号。在敏捷项目环境中,“t”是一个整数,因此在这里我们可以说,技术债务将随着时间的推移按几何方式增长(因为几何函数是指数函数的一个特定情况——当指数函数中的“t”永远取整数值的时候)。
因此,随着混乱的代码库不断积累,系统将变得更加脆弱且难以维护。技术债务利息增长的另一个副作用则是,由于用在维护上的时间越来越多,导致团队生产力遭到了抑制。
在某个项目中,我们在很长时间内都在忍受这样的糟糕代码实践,当我们最终进入正式投入使用前的阶段时,系统突然之间就崩溃了!我们不可能进行重构的同时,又能够避免在系统中诸多部分带来重大影响,因此我们决定一切推倒重来!
下一节将简要介绍一套用来管理技术债务的推荐流程。在初始假设中,我们认为技术债务是一种风险。这项假设基于对风险的定义:“可能会影响至少一项项目目标——范围、计划、成本或质量——的一件不确定事件。”技术债务非常符合这条定义,因为它对项目来说是一项潜在威胁,如果不能及时解决的话,可能会对项目造成负面影响。
作为敏捷爱好者,我将把这个流程放在Scrum项目中,来进行分析。实际上我发现,非常有必要管理敏捷项目的技术债务。因为与其他方法相比,敏捷方法中的快速交付节奏更加鼓励快速且不干净的代码风格。而且在敏捷项目中,我们只进行恰到好处的设计和架构,并通过重构来跟上任何需求的调整;这样的后果是,在某种程度上我们总会拥有技术债务,因为我们总是不得不在展开设计的过程中优化我们的代码。
技术债务管理流程如下:
设定技术信用限额(TCL)——TCL是我们愿意借出的理想工作小时数或用户故事点的最大总额。可以用总项目大小的百分比形式来计算该限额,例如10%。
识别技术债务因素——技术债务因素是指这样的情况:某位团队成员希望绕过一些良好的代码、设计或测试实践,以便实现快速交付。我们应该在每天Scrum会议通过小组讨论来识别这些因素。
记录技术债务任务——对于每项技术债务因素,需要在技术债务记录里添加两项任务,并使用立项工作小时或用户故事点的方式来估算其大小。这两项任务是:
开拓型任务:指我们决定利用技术债务因素时,需要做什么。这是技术债务的累加。
偿还型任务:当决定重构代码或设计的时候,我们需要做什么。这一任务的大小,代表了我们从TCL中拿出多少来偿还发生的债务。
选择任务——在Sprint规划会议中,选择希望开拓的技术债务因素。技术债务因素的选择应该基于产品所有者确定的优先级。
从TCL中减去偿还型任务所关联的技术债务的大小(限额扣减)。
在Sprint待办事项列表中增加与开拓型任务相关的技术债务,并将其大小累加到项目总大小上。
如果我们发现TCL已经快要用光了,那么接下来我们需要:
在Sprint待办事项列表中添加一项偿还型任务。
在完成该偿还型任务后,对TCL增加等量额度。
这样,我们将TCL作为监视系统,以便在技术债务开始积聚的时候警告自己,以便我们努力使其恢复到健康水平。
结语
将技术债务作为风险来进行管理,并使用技术信用限额,能够有效地减少技术债务的负面影响,同时令收益最大化——特别是在敏捷方法中,我们很容易滥用技术债务,将其作为一种加速交付的手段,因此也就需要更加关注技术债务。