程序员遇到祖传代码:技术债是推翻还是维护?

近日,Reddit 上有关 技术债务 的话题再次引起程序员的广泛讨论,面对由错误的或不理想的技术决策所累积的债务,程序员到底是该继续维护还是推倒重写,这个决定应该依据哪些因素来最终决定?如何避免在技术债务上浪费过多时间?本文,InfoQ 针对这一话题采访了多位国内技术从业者,试图对这一问题进行深入剖析。

你在写技术资产还是技术债务?

技术债务是由 Ward Cunningham 在 1992 年的报告中创造的比喻,被定义为当我们有意或无意地做了错误的或不理想的技术决策所累积的债务。简单来说就是为了快速解决问题而采取的不规范方案,比如开发工程师将某个判断条件写死、测试工程师未进行深入自动化测试、架构师运用了一个即将过时的框架。

对任何一家公司而言,在不断发展的过程中会出现很多“技术遗产”,这其中的部分遗产没有文档,甚至连注释都语焉不详,一旦引入新的需求和框架就可能出现混乱、冲突等,这部分代码很难将其称之为“技术资产”,称作“技术债务”会更加准确。

在知乎上,同样聚集了很多工程师对这一问题发表看法,比如:

写代码不写文档和测试用例,写出来的代码就不是资产,而是技术债务。

开发人员的工作比较多面,一方面开发新的需求,另一方面又要维护他人遗留代码。

作为运维,技术债务让我每天需要进行频繁的 BUG 修复上线

传统观点认为,工程技术团队应该为代码库(也就是技术债务的所处环境)建立一种直观的感受,了解其对公司的影响,而后在组织内建立信任。如果首席架构师强调重构核心代码,那么,开发者通常就得按照指示行动。诚然,如果公司可以对技术债务建立起一种共识与信任文化,这将有利于挽留优秀的工程师,并保持业务良好运作,但这往往需要多年努力。

重构 or 维护,这是一个问题?

即便技术债务让很多开发人员不爽,但是要想明白完全推翻还是继续维护,依旧是一个巨大的难题,这既需要考虑现有业务的稳定性、也要平衡后续的开发和维护成本。通常,如果业务可以正常有序的运行,管理者很难主动提出重构整个代码。但是,如果不对技术债务进行妥善管理及合理规划,组织或者开发人员很可能会陷入崩溃。

重构,就是在不改变外部行为的前提下,有条不紊地改善代码。为了保障软件的外部行为,唯一的办法就是通过测试。因此,重构是建立在完备的测试覆盖基础之上的。如果不能保证修改后的代码还能提供相同的功能,那么这种修改就可能是错误的,会给用户带来极大损失。在有风险意识的团队中,不会同意盲目重构。

即便公司有完备的测试,但如果重构花费时间周期太长,还是很危险,开发人员不得不在这段时间内同时应付重构工作和新功能的开发。框架迁移就是一个典型的例子,如果打算把旧框架的功能迁移到新框架,那么几乎所有功能都得在新框架下重新开发并测试一遍,新需求也不得不在旧框架中完成,并且最终还得再迁移过去。

所以,组织应该如何进行抉择呢?这里可以参考谷歌公司站点可靠性的 例子 ,谷歌的搜索引擎其实并不追求 100% 正常运行。这是因为,99.99% 的正常运行足以让用户把谷歌评为“极其可靠”的服务提供者,而由于最后 0.01% 的指标太难达到,所以根本就不值得为其浪费时间。

因此,如果每年有 52 分钟的计划内停机时间,谷歌会尽可能实现这一目标。而低于 52 分钟的目标都是在浪费时间,相关成本太高,包括无法承担额外风险以及不能为客户额外提供更多功能。

如果将技术债务预算类比成站点可靠性预算,假设目前正在承担的技术债务非常关键,而且开发人员很清楚其能力低于客户与业务能够承受的最低标准,那么应该尽可能弥补,如果预算不允许,可以先考虑偿还其中一部分。如果预算充足,则可以上调风险与债务比例。

总而言之,目标是尽可能保持技术债务水平接近理想程度。换句话说,如果处于上图的红色峰值部分,那么理想的技术债务预算应该是 A 到 B。如果处于绿色峰值部分,那么理想的预算则为 B 到 C,尽量不要选择 A 到 C,这样的预算额度太过激进。

AngelList 公司的 Andreas Klinger 曾在文章中提到:

“很多东西其实没有必要重构。如果其并不关键,或者未来几个月内并不需要改进其功能,又或者其太过复杂,那么将其纳入技术债务即可。”

简而言之,应该首先确定需要在本周、本月或者本季度完成的目标,与其存在技术债务的代码库之间的交集,只偿还交集之内的债务,其它的以后再说。除了重构,团队也可以选择将非核心技术栈外包出去,转交给更合适的团队。需要注意的是技术负债具有利滚利效应,偿还周期越长,所需偿还的债务总量就越多。

如何避免过度浪费时间?

即便是技术债务,现在也有很多指标可以帮助量化分析,避免过度浪费时间。决策者可以利用数据分析快速确定需要尽快偿还哪些技术债务,比如:

  • 识别代码库中归属关系较弱的文件,因为代码归属权是代码库运行状况的主要指标。
  • 衡量文件的内聚与耦合情况,并最终列出一份包含弱归属权、低内聚与高耦合文件的列表。
  • 计算各个文件的组成以确定问题文件中的各个子集。正如微软研究院所指出,“活动文件仅占系统总体大小的 2% 至 8%,但占系统文件变更的 20% 至 40%,而且有 60% 到 90% 的 bug 来自于此。”
  • 将这些文件与本季度的发展路线图进行比较。在路线图当中列出的所有功能,是否都要求工程师对问题文件子集进行调整?如果答案是肯定的,请将这些文件作为重构对象,估算相关工作量,并将其分配给文件所有者的工程师。最后,把这部分工作量纳入下阶段计划。

国内技术从业者怎么看?

针对这一问题,InfoQ 采访了国内一些技术从业者,针对技术债务的判断原则、分类、应该采取哪些措施以及如何避免在技术债务上浪费过多时间等问题进行了交流。

刘译璟,百分点 CTO

技术债是指在软件工程中“应该”做而“没有”做的那些事情,简单说就是没有兑现的那些承诺。在软件工程的各个环节都有可能存在负债,包括:需求分析、方案设计、架构设计(逻辑架构、功能架构、数据架构、部署架构、运行架构等等)、编码、测试、发布,以及整个软件工程的流程都有可能是存在不合理的、负债的。

判断技术债务的重点在于“哪些事情是应该做的”,它是一个因组织而异、因项目而异、因人而异的过程,例如以下一些方面:

  • 组织上要求做但没做的:制度、流程、规范、分享学习等;
  • 业务和技术上要求做但没有做的:功能、性能、安全、高可用、扩展、监控、辅助工具等。

如果按照软件工程环节分类,技术债务可以分为:需求分析、方案设计、架构设计(逻辑架构、功能架构、数据架构、部署架构、运行架构等等)、编码、测试、发布等。如果按照产出物类型分,可以分为:

  • 文档类:管理过程文档、需求分析文档、设计文档、测试案例文档等;
  • 代码类:代码、脚本、规范等;
  • 软件包类:产品软件包、依赖软件、依赖资源等;
  • 环境类:开发环境、测试环境、预上线环境、生产环境等。

至于如何决定要重写还是继续维护,需要判断“继续维护的收益”和“重写的收益”哪个更大,来决定继续维护还是重写。可以综合考虑如下几方面的收益:

  • 开源:提升现有业务收入、支持新业务的开拓;
  • 节流:节省维护人员、节省运营费用;
  • 组织:人员结构调整、组织能力培养。

债务是避免不了的,时刻判断“持有债务的价值”,当价值很低时要尽快处理。

王辉 腾讯研发总监

简单来讲,债务的基本解释是“债户还债的义务,有时也指所欠的债为了清偿所有债务而工作”,那什么是技术债务就很好判断了,就是必须为之前所做的技术决策、方案或编码重新投入人力、物力去弥补(俗称“擦屁股”)的事情。例如,服务再不进行优化,流量上来,就要彻底宕机;由于追求短期快速上线,方案粗糙,上线后项目质量很差,必须投入非常多的精力和人力对用户投诉和问题修复等,这些就是技术债务。这就意味着,许多我们之前归咎于“技术债务”的事情实际上根本就不是债务,比如,随着版本的迭代,项目臃肿,无法保持最佳编码实践,但整体还是在之前设计的方案中运行的,就不算技术债务。

综上,团队为了短期利益、因为技术人员能力或者历史原因,造成的必须为之付出代价的事情,就是合理的技术债务。

技术债务通常可以分为:

  • 因为短期利益,追求快速上线导致的粗制滥造;
  • 因为人员技术能力和疏忽,导致的项目质量差;
  • 因为方案选型不当,方案设计不合理导致的推倒重来;
  • 项目合并、团队合并带来的代码融合,引起的效率低下。

如果人力、物力和工期等资源丰富,这种能去优化的就都可以做到极致。但通常,资源都是不丰富的,或者说是捉襟见肘的,那就要根据实际业务情况来看,腾讯一向的方式是“先抗住再优化”,项目是否真的到了非优化不可的地步,就如上面所说,是否真的到了不优化随时都可能宕机的时候,如果先抗住了,就等业务占领了市场,站住了用户,到了项目进度慢下来之后,一些优化再开展起来,此时可以要求高可用、高性能、高并发等。

如果项目资源允许,一些稍微过度的优化和重构,个人认为是可以被接受的,保持团队的技术热情是不错的,但如果资源不允许,就要数着钱花,判断技术债务的合理性,如何更好的还债,是否真的到了非还不可,是否真的到了影响业务发展,需要与业务优先级一起看,业务错过一个时间窗就可能永远错过,有些技术债务还可以后期再还。

何恺铎,国双技术总经理

技术债务产生的本质往往是企业短期目标和长期利益冲突造成的,因为技术债务一般是老板和业务端无感的,这就需要技术负责人做好把控和平衡。我们也呼吁更多企业高层能理解技术债务这个事情,它也真的是企业债务的一种。

部分技术债务实质上和技术团队的组织架构密切相关,尤其对于大型研发团队,很容易出现各自造轮子的情形,造得不够好的轮子很容易成为债务负担。所以,在合适粒度上的基础设施团队对避免底层技术债务的产生很有帮助,前提是该基础团队要有一定影响力,也要善于沟通。

至于是继续维护还是推翻,肯定不能一概而论,要看对应业务是否有长期规划和决心,为了长期成功,可考虑择机重构,但如果业务本身有试水性质或者尚不明朗,那也许就先别折腾。

焦振清 资深技术专家

我个人理解的技术债务是因为现有或临时的解决方案无法适配当下的业务场景而导致的,随着两者的差距逐渐拉大,在达到一定程度后,就会形成明显的技术债务而需要去面对。举个例子,小马拉大车,初始阶段,资源有限,货物也不多,那么小马拉车这种方案没啥问题。但随着车上货物重量快速增加,小马托货能力却未明显提升,这样的组合就会出现小马拉不动大车。这时,要不把小马换做大马,也就是我们所说的还技术债务,要么预估货物重量不会继续增加,保持现状也没啥太大问题。

所以,是否需要面对技术债务,主要还是取决于当下的业务场景是否会继续发展。如果预估会继续发展,那么大多数时候,技术债务是要处理的,如果在重写和维护中选择,重写的概率会高一些。如果预估业务继续发展的可能性不大,而当下的解决方案存在各种隐患,那么在重构和维护中,选择维护的概率会高一些,甚至说继续保持现状也是极有可能的。

我们经常面临两难选择的地方在于,一个系统其发展前景已经被判处死刑,但是其依然有存量用户以及持续缓慢的用户增长,而这个系统存在较多技术债务,不去面对肯定会出问题,而如果去处理,是继续修修补补还是彻底重构呢?可能需要具体情况具体分析了。更多时候,大家会选择在这个项目上启用新人,通过该项目来达到锻炼新人的目的。

结束语

就这一问题,InfoQ 也在聚集了众多开发者的自有社区发起了话题讨论,通过互相交流,不少开发者更加认可“技术债务是有意做出来的,是人为选择的,而非无意犯下的”这一观点,这与开篇提到的定义略有区别,主要在于是否应该将“程序员无意为之的错误”认定为技术债务。

此外,有观点提到,关于技术债务的利息,很少有人提起,也几乎从来没人在决策时拿来做筹码,技术债务应该是有生命周期的,如果生命周期长于该技术产品本身的生命周期,就不值得改,就好比抽烟会减少寿命,但是明天地球就要毁灭了,那么今天再去戒烟就不值得了……但是,团队还是应该对技术债务具备一定认识,如果团队起点的不良代码就非常多,或者出现大量人员变动,还是应该将解决问题放在首位,避免互相甩锅的情况发生。