研发效能:如何平衡技术债务与业务交付?

发布时间 2023-11-28 23:31:30作者: 李若盛开

什么是技术债务?

技术债务也称为设计债务或代码债务,在技术领域被广泛使用。

信息与软件技术周刊如此定义技术债务:技术债务描述了某些软件开发行为所产生的后果,这些行为有意或无意地优先考虑客户价值和/或项目限制,例如交付期限、更多的技术实施和设计考虑……从概念上讲,技术债务是金融债务的类比,有着相似的概念,例如债务水平、随着时间的推移产生的利息及其可能的后果,以及在某个时间点偿还债务的压力。

之所以称之为技术债务,是因为它就像贷款一样,你当前可以完成比正常更多的工作,但最终会付出额外的代价。

技术债务一词最初是由Ward Cunningham 创造的,他不仅是敏捷宣言的 17 位作者之一,还因发明了Wiki 而闻名于世。Cunningham首先用这个比喻向非技术的利益相关者解释为什么需要为重构活动预留资源。Cunningham没有意识到自己创造了一个新的流行词,此后,技术债务成为无数学术研究、辩论和企业讨论的主题。

技术债务一词的流行与它所带来的便利有关,Donald G. Reinertsen强调采纳经济视角,技术债务的隐喻,用易懂的金融词汇描述了一个艰深但普遍存在的技术难题。与此同时,技术债务也明确的指出不完美的设计、代码的拥有成本、延误成本、长期持续的利息这几者之间的关系。

技术债务包罗万象,涵盖了从代码缺陷、架构不足、遗留代码、脚本、配置文件、基础设施,再到文档的所有层面。较为常见的代码和系统的技术债务包括:

  • 已知的未修复缺陷

  • 不充分的测试覆盖

  • 低代码质量和不良设计导致的问题

  • 不再使用但没有清理的代码/制品/功能开关

  • 团队缺乏领域知识,无法高效调试和维护

  • 未完成的迁移

  • 废弃的技术

  • 未完成或过期的文档或注释缺失

当你为了更快地实现目标,在编写代码时走了捷径,就会发生技术债务,代价是代码更丑、更难维护。

想象一下你有一个模块,因此前走的捷径,导致结构不够清晰导致维护和更新不便,当下新增一个功能需要6天的时间,如果结构清晰的话则只需要4天时间,那么额外增加的2天就是因为技术债务所产生的额外利息。重构这段代码也许需要10天,所以这10天就是债务的本金。是偿还10天的本金,还是持续每次背负2天的利息,这是一个技术选择,也是一个投资选择。投资的衡量来源于当下是否有本金可以偿还,这部分本金如果投资于其他领域是否可以获得更高的收益。

对技术债务的进一步分析

2007 年,Steve McConnell 提出有两种类型的技术债务:有意的和无意的。在他看来,有意的技术债务是人们有意识地将其作为一种战略工具来承担的技术债务。无意的债务则相反,他称之为“工作做得不好的非战略性结果”。

几年后,Martin Fowler 将 McConnell 的概念更进一步,发表了“技术债务四象限”,根据意图和上下文将技术债务分为 4 类。Fowler说技术债务可以首先根据意图进行分类:是有意的还是无意的?然后进一步区分是谨慎的还是鲁莽的债务。

 

谨慎的债务是经过深思熟虑的,因为团队知道他们正在承担债务,因此会考虑较早发布的收益是否大于还清成本。一个对设计实践一无所知的团队承担的是鲁莽的债务,甚至没有意识到自己陷入了多大的困境。

团队可能知道什么是好的设计实践,甚至能够实践它们,但决定保持“快速而肮脏”,因为他们认为负担不起编写干净代码所需的时间。这通常是一笔不计后果的债务,好的设计和干净的代码的意义在于让你走得更快更稳。

Martin Fowler的技术债务分类框架并未直接匹配债务的具体性质,2014年一些学者提议根据技术债务的性质而不是它是否具有战略意义进行分类,由此得到13 种不同类型的技术债务,每种类型都对应一组关键指标:

  • 需求债务

  • 架构债务

  • 构建债务

  • 代码债务

  • 测试自动化债务

  • 测试债务

  • 缺陷债务

  • 设计债务

  • 文档债务

  • 基础设施债务

  • 人员债务

  • 流程债务

  • 服务债务

技术债务的是与不是

技术债务不是一团糟的代码

不良代码不属于技术债务,长期担任软件开发顾问的Robert C. Martin(俗称Bob大叔)(同时也是《代码整洁》的作者)在一篇态度鲜明的博客中写道:“一团糟不是技术债务,一团糟只是一团糟。技术债务决策是根据实际项目限制和需要做出的。它们是有风险的,但可能是有益的。弄得一团糟的决定从来都不是理性的,是基于懒惰和不专业,并且在未来没有机会获得回报。一团糟总是一种损失。”

技术债务应该是人们经过深思熟虑,决定采用长期不可持续但会产生短期收益的设计策略。核心关键是债务会更快产生价值,但需要尽快安排偿还。

技术债务不是缺陷

讨论缺陷是否是债务,是一个错误的论题。很多人误解了债务的比喻,将其与浪费混淆了,技术债务容易与缺陷混为一谈。大多数程序员会说:“好吧,我只需要在最后期限之前快速完成,稍后我会回来修复它。” 很多人认为只要目的是更快的做好工作,就可以编写充满缺陷的代码,并认为这就是技术债务的主要来源。

编写糟糕的代码从来都不是什么好事,但我们赞成基于当前对问题的理解来编写代码,以尝试获得反馈进行验证,即使这种理解是片面的。明智的做法是该软件尽可能清晰地反映你当下的理解,以便在需要重构时,很清楚编写它时的想法,从而更容易将其重构为你当时的想法。换而言之,偿还债务的能力,取决于你编写的代码是否足够干净,以便在你理解问题时能够进行重构。这也是为什么鲍勃大叔的《整洁代码》和Martin Fowler的《重构》一书同样的重要。

技术债务是有意而为之的

正如明智的金融债务可以帮助更快地实现主要的人生目标一样,并非所有技术债务都是坏事。管理好它可以为公司带来巨大的收益,对于快速发展的公司来说尤其如此,迫切需要尽早并经常发布以确定产品/市场契合度,满足客户需求并抓住市场机会。

技术债务描述了当开发团队采取行动以加快交付,稍后再进行功能或产品重构的场景。换句话说,这是优先考虑快速交付而不是完美代码的选择,或者说是在速度与质量之间的权衡中倾向于前者。既然是选择,就应该是有意而为之的,而非无意识或下意识的举动。

Shaun McCormick 对技术债务的定义更多地关注长期的后果,“我认为技术债务是随着项目的成熟而降低敏捷性的任何代码。请注意我没有说坏代码(因为这通常是主观的)或损坏的代码。” 他建议真正的技术债务总是有意的,而不是偶然的。

承担技术债务始终是有意和战略性的,故意背负的技术债务,是人们有意识地将其作为一种战略工具来承担的债务。无意的债务则相反,这是工作做得不好的非战略性结果。

技术债务是经济选择

Cunningham描述他最初是如何提出技术债务比喻的:“有了借来的钱,你可以更快地做一些事情,但是直到你还清这笔钱,你都需要支付利息。我认为借钱是个好主意,我认为将软件推向市场以获得一些经验也是个好主意,但是当然,当你获取了相关信息之后,需要通过获得的经验来重构程序,以偿还债务。”

如果你有一个梦魇一般的模块,无人愿意触碰,你是选择对其进行重构,还是听之任之?

技术债务的概念让人们真正从经济上考虑这个问题。Reinertsen说,“如果你只量化一件事,量化延迟成本”,同时根据最佳决策时机原则,每个决策都有其最佳经济时机。最佳经济时机的衡量需要综合考虑成本与收益,比较边际成本和边际价值,同时忽略沉没成本。

上述问题的核心,在于重构的成本,以及因为此模块而背负债务的利息。利息与维护和更新的频度有关,与单次的维护成本有关,与已投入的研发成本无关。

技术债务不可避免

软件的真正成本是时间。所有软件都是为了解决问题而设计的,今天解决一个问题比明天解决它更有价值。因此,在任何工程项目中,尽快完成都至关重要。快速而不完美的代码,具有最多技术债务的代码,实现了这一点。如果在每一行代码都完美无缺之前拒绝发布软件,你的产品可能永远不会交付,你的公司会很快接近尾声。

不存在完美的产品,我们是在未知的不确定性世界中构建软件。软件的客户对他们需要产品中的哪些功能只有一个粗略的想法,并在软件构建过程中逐渐了解更多的信息,尤其是在向用户发布早期版本之后。“我们做出了很好的决定,但直到现在我们才明白我们应该如何构建它”,对于产品和架构,只有产品发布之后才会获得足够多的信息,以便更好的指导构建。

所以我们不可能也不需要一开始就把它设计得最好,我们无法避免技术债务。债务比喻提醒我们可以针对设计缺陷做出选择,快速交付获得的收益足够大,并且利息支付足够小,或者债务位于代码库中很少涉及的部分,那么债务可能不值得尽快的偿还。

当你在编程时,你是在学习。通常情况下,在了解最佳设计方法应该是什么之前,甚至可能需要对项目进行超过一年的编程尝试。技术债务是不可避免的,所以是可以预料的。随着项目的进行,即使是最好的团队也会有债务需要处理,更有理性而非鲁莽的,用清晰但不完美的代码背负它。

有技术债务是好还是不好?

如果你想要一个简单的答案:技术债务没有好坏之分,它就是债务,就像金融债务一样。

关于技术债务是好事还是坏事,有多种不同的观点,与其寻找客观唯一的答案,不如在这里讨论这些不同的观点。

如今,大多数软件公司都面临着来自市场和竞争力的压力,需要快速开发和发布。初创公司尤其能感受到这种压力,这不是To be or not to be的问题,而是必须快速发布。这种对速度的需求导致许多产品和软件开发团队在承担技术债务或稍后发布之间做出权衡。这就是为什么大多数敏捷团队的普遍共识是技术债务本质上并不是坏事。事实上,大多数软件产品,如果不是全部,都有一定程度的技术债务,这并不难理解。

对技术债务的态度因公司理念而异,因研发模式而异,因业务模式而异,同时因部门和角色而异。业务人员对技术债务的容忍度普遍高于技术人员,因其业务属性,业务人员经常需要进行经济层面的权衡。而技术人员通常容易存在完美主义执念,似乎认为唯一正确的是技术债务为零。这种对技术债务的不同理解,不可避免地会产生沟通挑战。

技术人员通常会向业务人员解释什么技术债务,但事实上,技术人员可能并未真正看到其中的含义,主要问题是,与金融债务不同,技术债务并非显性的存在,因此人们尝尝容易忽视它。

所以核心要点是,在确定技术债务是好是坏时,时机很重要。一般而言,你可以像处理金融债务一样来考虑技术债务:它的存在不会是问题,直到它成为问题。

忽视技术债务的后果

任何被忽视的技术债务都可能会给组织带来问题,形成技术债务的负向循环。

阻碍新版本的发布:当技术债务失控并扩散时,团队大部分时间都花在偿还前面项目的“利息支付”上,而不是致力于新功能或关键的更新。最终你必须偿还技术债务,它是以减少添加新功能或进行其他更新以推动产品向前发展为代价的。技术债务使得改变现有源代码的难度和风险呈指数级增长,这反映在产品和市场上。更长的交付时间会减少市场机会窗口并增加新功能的上市时间。

设计糟糕的代码:在采取快速而肮脏的方式到达最后期限时,开发人员会跳过编写干净、有组织的代码的编码规范。糟糕的设计,代码结构将变得混乱并且具有潜在的低可读性、可维护性、可扩展性。

软件熵增架构混乱:当诸如遗留系统等软件随着时间的推移,由于过时的设计、临时的修复等原因,代码在一段时间的演进后缺乏有效的整理,就会堆积技术债务。随着公司和产品的发展,这种情况最容易解决,但很难完全避免,最终将导致产品的性能下降。

逐渐枯竭的生产力:技术债务会耗尽公司的生产力,从而导致产出放缓。设计糟糕的代码需要团队花费更多的时间和精力来管理它。技术债务导致团队减慢他们的构建时间,减慢整个生产测试发布周期。根据 2018 年的Stripe报告,大多数开发人员有将近三分之一的时间在用于处理技术债务。技术债务每周平均窃取 3.8 小时,这是阻碍公司开发人员生产力的主要原因之一。这意味着每年有惊人的 850 亿美元全部花在……糟糕的代码上。

客户/员工满意度下降:系统停机、不稳定的性能或糟糕的用户界面会影响用户满意度,从而对你的产品和业务产生负面价值。技术债务不仅会给客户带来负面影响,同时也给 IT 部门带来很大压力,因为它非常耗时并且严重阻碍进度。换句话说,技术债务不仅会消耗工程时间,还会导致开发团队失去动力。结果,创新被淡化了,员工士气和忠诚度都会降低。

承担技术债务有时可能是好的决策,但是过多的技术债务会降低团队的敏捷性,产生低质量的代码,给测试团队带来压力,并最终降低公司的整体生产力。将技术债务视为一把双刃剑,可以用以决策产品发布周期,于此同时理解它、使用它并从长远的角度来管理它。

如何对待技术债务

让我们回到信用卡的比喻,当存在债务时,每个月信用卡上待还清余额将随时间增长,可用额度将随之降低。那个每次都选择支付最低还款额度的年轻人很快就会学会,你等待还清余额的时间越长每个月的最低还款额度将越来越高,可以腾挪的空间越来越小,偿还起来越加的困难。

技术的债务也是如此。随着时间的推移,技术债务的积累会产生越来越高的风险,这就是为什么你需要经常(通过重构)偿还债务。通过定期对应用程序进行小的内部修改,通过重构,你可以使自己的产品保持简洁和优雅,并且更容易添加想要的新功能。

考虑到业务和产品团队以及其他利益相关人,他们更希望的是尽快发布新产品,缩短开发周期。事实上,开发团队同样的热衷于新功能的开发,重构一类的技术需求无法直接体现到业务价值上,即便是在技术的反馈上通常也会较长。

团队没有定期进行微小但重要的重构,产品的代码库变得越来越笨拙,并且随着一次又一次的次优选择,在与完美背道而驰的道上一路狂奔。

软件时代的魏文侯问扁鹊:你兄弟三人,哪个软件开发水平最高?”扁鹊说:“大哥最好,二哥其次,我最差。”文侯甚为不解,扁鹊解释道:“我大哥不等Bug出现就提前预防了,所以名声传不出开发组。二哥在Bug出现时就顺手解决了,所以名声传不出技术部。我呢,每天像打地鼠一般到处救火,所以在公司没人不知道我。” 最后大哥因为每天整点下班而不是熬夜加班,而且上班时看着也不忙,领导认为他不认同企业文化,找个借口开除了。

上面的虽然只是玩笑,却实实在在的体现了软件开发中的一些普遍现象,能够防患于未然的,未必是“影响力”最大的,救火队员往往内卷到极致,还喜欢把自己的辛苦各种晒圈,“0点的XXX”已经是常规动作,凌晨4点洛杉矶的科比,真的有可比性么,你们真的有那个本事么?

技术债务在初期不会造成严重影响,甚至理论上应该产生相应的业务价值。随着臃肿的代码越来越相互交织,尾大不掉,积重难返,对业务响应速度的影响越来越凸显,最终,当业务和产品团队想要对产品进行重大升级或构建重大增强时,代码和架构终于因无法支撑新的业务诉求而轰然倒塌,此时进行重构已为时过晚。通用的做法是进行大版本升级,这也是为什么我们经常听不到频繁重构的优秀实践,却常常能看到大肆炫耀大版本更新的成功案例。毕竟,一个大版本的升级所带来的成就感,及其随后带来可吹嘘的政治资本,要远远超过“小小”的重构。

架构缺乏维护和治理,代码缺乏定期的重构和清理,就如同混乱的房间一样,并非一朝一夕形成,一旦形成,想要让其变得整洁就愈发的困难。乱序的状态遵循热力学第二定律,即熵增定律。而引入熵减机制,是需要耗费能量的,这也是违反人类长久以来形成的“能不用脑就不用脑”的节约能量的本性。

通常最好的方法是像我们通常处理金融债务一样,逐步还清本金。当一个团队能够开始偿还技术债务时,他们不仅是在偿还债务,同时也将开始从债务偿还的循环中受益:你偿还的每一笔债务都会导致有更多的时间偿还更多的债务。这是一个正向的循环飞轮。

如何偿还债务,Martin Fowler的建议是通过重构,“重构是对软件内部结构所做的更改,以便在不改变其可观察行为的情况下更易于理解且修改成本更低。”

如果做得好,你的团队将持续对代码库进行这些小的更改,而不会产生任何外部后果——这意味着它们不会中断、减慢速度或以任何方式破坏用户对你的应用程序的体验。这意味着你能够,并且也应该随时进行重构。

将重构构建到你的日常流程中,意味着需要给它预算——时间、资源、冲刺计划看板上的空位。正如自动将 10%的薪水投入长期投资可以随着时间的推移极大地增加你的财富一样,在团队日历上固定分配时间进行重构,可以在长期内为产品的健康和整洁带来巨大的红利。

技术债务管理需要在质量(包括用户的良好体验)和速度之间取得平衡,以满足业务最后期限。如果不加以控制,技术债务乍一看似乎无害,但速度和敏捷性将随之逐渐丧失。当企业专注于缩短上市时间并允许对质量缺乏敬畏之心的非专业开发人员自行创建软件应用时,技术债务的威胁就会增加。初级开发人员可能缺乏专业知识,并且容易忽视债务,直到债务失控。解决方案的捷径可能会让你快速实现目标,但要注意长期成本,以及识别和修复技术债务。

以下是有效对待技术债务的一些建议:

  • 识别和评估

1. 认清你的技术债务。公司经常在没有意识到的情况下积累技术债务,从而成为一个水面之下的问题,变为诸多问题的诱因。你越早认识到它,就越容易还清你的技术债务。

2. 记录并标识你的技术债务。在统一的系统记录你所有的技术债务,定期审视,确定债务当前状况。

  • 修复和还债

1. 制定还债策略。

根据你的技术债务水平,当前的财务状况(资源状况),变更频度影响程度,基于此制定相应的偿还策略。最佳的方式是分阶段重构,通过专注于在每个冲刺少量的还债,你可以有效的逐步减少技术债务。还债的投入一开始可能会很昂贵,但从长远来看,它会赢得回报。

  • 定期重构:

1. 定期重写软件中的组件,重构代码作为有效的预防措施,有助于代码随着时间的推移带来的逐步腐蚀,增加其耐受性,聪明的团队不会等待系统崩溃,而是先发制人地采取行动来提高代码的免疫系统。

2. 为技术债分配专用力量:将中等大小的技术债当作日常迭代的一部分,以一定比例的工作时间用于这类的活动。一种常见的分配是 70% 用于功能开发,20% 用于技术债,10% 用于学习 / 实验。另一种方式是不讨论投入的时间,只是从每个工期的积压工作中拿出固定数量的技术债务。一个明显的问题是有些技术债问题可能很大,这时候需要进行拆分,或者如果真的必要,拿出一大块时间,将比较重要的技术债务当作项目或者独立的重构迭代。

3. 利益相关者:确保你的利益相关者了解重构的必要性,你可以尝试用信用卡的例子来解释什么是技术债务,并且在近期的迭代中先预留出少量的时间进行重构。

  • 预防和改进

1. 强调代码纪律:如前面所述,糟糕的代码不等同于技术债务,但其导致的后果与技术债务一样。代码开发的“童子军规则”是,你应该让代码库和系统变得更好。拥有一个严格遵循编程要求和规范的成熟开发团队对于避免代码债务至关重要。在没有工程质量要求并且重视代码纪律的公司,技术债务也很常见。此处强烈推荐Bob大叔的《代码整洁》一书。

2. 构建你的工程脚手架。敏捷与DevOps相关的工程实践对于重构活动而言形成了有效安全的脚手架。例如,采用TDD的方式进行开发,能够对每一次微小的重构进行保护;采用持续集成的实践,可以快速且高度一致的自动化手段。更快的步伐并不意味着粗心的工作,敏捷也不是“快速而肮脏”的代名词。相反,敏捷方法(无论是管理实践还是工程实践)是技术债务的克星,频繁迭代,一次处理小块工作,测试自动化,架构模块化等等。

3. 灵活松耦合的架构:与代码质量一样重要的是强大的架构 ,它允许你从轻量级设计开始,然后逐步增长,并积极跟踪和管理不可避免的技术债务。采用微服务的松耦合架构和接口定义和接口测试,可以有效将变更影响隔离在微服务内部。

总结

IDCF的研发效能原则中,很重要的一点就是采纳经济视角,技术债务是绝好的从经济视角来看待软件问题的一个示例。

软件开发是一场投资,也是一场赛跑,用有限的时间与市场和商机赛跑,业务通常高于一切,但需要在风险可控的范围内。应该具备经济头脑,不是冒险一路狂奔,需要思考如何用相对保险的债务来作为杠杆,去调动更大的业务机会。用一定周期范围的负债,来换取时间成本和机会成本。

处理不当的技术债务,会导致阻碍新版本的发布,设计糟糕的代码,软件熵增架构混乱,逐渐枯竭的生产力,客户/员工满意度下降等诸多问题。

技术债务应该是有意而为之的,并非无意识更不是无意义的浪费。背负债务的多少,不能让偿还利息将你的腾挪变得困难。债务需要定期的偿还,虱子多了不痒,但是会溃烂。破窗效应,童子军法则,代码基本的守则还是应该在日常活动中建立并遵守。

技术债务并非一刀切的好或者不好,需要看债务存在的理由,以及对其有效的治理。对于技术债务的治理,通常有识别和评估、修复和还债、预防和改进等三个方面。

参考资料

https://www.productplan.com/glossary/technical-debt/ 技术债务的定义

https://martinfowler.com/bliki/TechnicalDebtQuadrant.html Martin Fowler关于技术债务四象限的博客

https://martinfowler.com/bliki/TechnicalDebt.html Martin Fowler关于技术债务的博客

https://www.productplan.com/learn/refactoring/ 产品管理者如何为重构留出预算