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