一、什么是技术债务
技术债务被称为“债务”,因为你要缴利息。打个比分如果你借钱,你就得缴利息。只要你不还债,利息就会加到你的收入中。如果你还让利息加到债务中,那么随着时间的推移,利息本身就会超过你的收入。
技术债务也不例外。只要你能还清债务,承担债务可能是一个明智的决定。拖得越久,成本就越高。如果你让利息堆积在现有债务之上,你最终会陷入这样一种境地:你所做的只是处理债务,而无法为你的软件增加任何价值。
越过这种“技术债务事件视界”的公司往往无法摆脱困境。尤其是当那些贡献最大的人最终厌倦并离开时。然后你就会陷入“死海效应”的循环,你甚至无法留住那些能够让你摆脱困境的优秀开发人员。
如果你曾经听过开发人员谈论他们如何花费几乎所有的时间“修复错误”,那么这就是问题所在。而且这种情况发生得往往比人们(尤其是经理)想象的要早/要快。
举个例子:
- 在一个维护良好的系统中可能需要 1 小时才能完成的事情,在一个混乱的系统中可能需要 3 小时,在一堆乱七八糟的东西中则可能需要 10 小时。需要在每个维护要求中预算的额外时间就是你为偿还这笔债务而支付的利息。就像金融债务一样,利息越高,偿还的优先级就越高。
- 我有工作要做。为了完成这项工作,我必须了解系统、对系统进行更改、验证更改是否有效,然后部署更改。
在我看来,任何妨碍我完成工作的事物都是技术债务。为什么这么说呢?
- 系统是否很难理解(例如,我是否必须遵循 50 个方法调用才能理解变量的使用方式)?技术债务会减慢我的理解速度。
- 对系统进行更改是否会导致系统或其依赖项发生意外或复杂的变化?技术债务,因为它很难改变。
- 我是否必须启动整个生产堆栈来测试更改,因为系统依赖于整体的世界观才能进行测试?这是技术债务,因为这会减慢我的验证速度。
- 我从未接触过的系统?不是技术债务,因为它不会妨碍我做我的工作(尽管它可能有潜在的技术债务,这会让我以后难过)
1.1 只有需要偿还的债务才叫技术债务
我经常进行重构。构建某件事物时,我的第一个假设至少有一部分是错误的。我花了很多精力将代码设计得易于重构。
引用《README Missing Manual》一书中的话:只有当你必须偿还时,技术债务才是技术债务。
假设你写了一些可以完成任务的粗糙代码,但你永远不需要再碰它。这不是技术债务。
假设你编写的代码在某种程度上给你带来了操作负担。处理混乱代码带来的操作负担所花费的时间就是你为债务支付的利息。如果你永远不需要支付任何利息,那么它就不是债务。
所以不要过度使用这个术语来标记所有糟糕的代码。
我认为,归根结底,代码比实际更难阅读或编辑。有时代码可读性强,但为了进行一次更改,必须编辑大量文件,我认为这是技术债务。有时可以完全不重复,但代码很难理清。关键在于易于阅读,同时保持灵活性。
我真的很喜欢将功能/系统/视图/等设计为可替换而不是可重复使用的经验法则,因为后者通常会导致过度设计。
1.2 技术债务是主观的
“债务”有时是对技术债务的一个误导性比喻。它在大多数情况下是有效的,但它的缺点是:技术债务是主观的,对于不同的个人或团队来说可能更高或更低。当一个新人开始在现有的代码库上工作并立即宣布它背负着债务时,你最清楚地看到了这一点!
换个角度来思考:技术债务是系统思维模型与实际实现方式之间的差距。如果我接手了一个用大量 Java 风格 OOP 编写的系统,我会很难继续工作,不是因为它的代码不好,而是因为我不习惯用这种方式思考。
这就是为什么很难制定技术债务指标的原因——每个人的指标都不同,我们能做的最好的事情就是弄清楚代码对于“普通”开发人员来说有多难理解。
减少技术债务影响的一种方法是永远不更换团队,让人们长期拥有一个系统。这种方法非常高效,但非常脆弱,如果人们决定离开,你的“巴士系数”就会很低。另一种解决方案是定期调动员工,并确保你有良好的架构文档,这样新人就可以更容易地推理事情。
1.3 技术债务的两种类型
技术债务有两种类型:一种是每天都拖慢团队进度的任何事情,要么是由于问题不断出现并占用支持时间,要么是由于复杂性拖慢了功能工作。有时,你对系统已经习以为常,以至于这种拖累团队的现象变得不可见,直到有新人加入团队,并问这到底是怎么回事?有时,开发人员对是什么拖慢了他们的进度有不同的看法。
第二种是迫在眉睫的灾难。可能会发生数据泄露/丢失,某些系统会达到阈值并发生故障,您没有时间编写测试的功能将开始出错,或者唯一真正了解关键系统的开发人员辞职。现在一切都很顺利,但如果你不主动解决问题,它就会给你带来麻烦,而且它会在最不方便的时候发生。
高级开发人员、工程经理或团队负责人应该指出这些问题并制定解决方案,因为当这些问题自食其果时,你最好相信他们会承担责任。
二、技术债务在你日常工作中
技术债务在公司所有职场角色中都会存在。我来针对每个角色说说几条,看你命中了几条
2.1 作为一个RD
- 前端的每个反应组件都位于一个名为 components 的文件夹中,实际上有数百/数千个文件。
- 与某个结构/类相关的每个辅助函数都在一个文件中,无论这些函数在哪里使用(可能只在某些角落的代码路径中使用一次)
- 有一个地方列出了整个后端代码库中用于日志记录/监控的所有可能的“标签”,并且该地方与实际执行日志记录的函数在一起
- 我不知道该如何称呼这种模式,我也见过一些关于它的争论,但在我看来,这与模块化代码库的作用完全相反。你必须知道/阅读很多东西才能修复一个问题,而且很难发现问题
- 庞大的百万 行单体代码库 - 一个大型的 monorepo
- 所有内容都用旧版 Perl 编写 - 0 个单元/集成测试
- “模块”(类)长达数千行,几乎没有任何注释
- 前端是一个自定义的 JavaScript 框架,因为技术主管讨厌与 Nunjucks 集成的 React/'现代 JS' - 完全损坏并且是一堆混乱的 jQuery 和 Underscore,旨在与自定义编写的 CMS 一起使用,其中所有代码都在一个大文件中。
- 每次我们创建新网站时,都需要使用版本控制有问题的CMS。网站只是基于“基础框架”的相互复制,因此每次创建新网站时,它都会包含其他网站的所有错误,必须一遍又一遍地修复它们。
- 忘记 ES6 吧——一切都必须用旧式的“原始” JS 编写,因为他们使用的是雅虎大约 10 年前构建的 JS 压缩器,而 Debian 系统太旧,无法安装 NPM。
- 大量内联 <script> JavaScript
- 页面使用自定义的“块”系统加载,其中 HTML 文件从文件夹加载(想想 PHP include()) - 通过创建无限包含循环可能会破坏整个系统。
- 几乎没有任何文档
- 添加功能需要更改不明显的次要位置。
- 有一个插件系统,但有些地方有一个硬编码的“已发布”插件列表。
- 基础设施平台和功能开发之间存在分离,但不知何故基础设施代码if (feature.flag) { magic; }直接分散,而不是可插入的扩展点。
- 将本地化的文本按钮更改为通用图像,但这需要添加到所有语言环境。
- 我想添加一个布尔值,并且需要将其添加到代理微服务长链中的每个 Protobuf 消息中。
- 我想添加一个语法规则,结果发现编译后的语法已被签入,但没有人知道如何重新编译它。
- .软件的一半都是用 vb 编写的,变量名是“var1”或“b”。一些较旧的存储库中有评论说对它们的修改可以追溯到 20 年前。
2.2 作为一个RD骨干或者小leader
- 一切都按“它是什么”耦合在一起,而不是分成模块/子功能。
- 错误跟踪器中有 500 个错误,但添加新功能是优先事项
- CV 驱动开发。开发人员 A 决定尝试用 Vue.js 编写 SPA,以便将其放在简历中。因此,他查看了积压工作,找到了第一张与前端略有关系的票据,并将其编写为 Vue.js SPA。现在我们有 N 1 种语言、框架和范例。重复 X 次,就会发现我们陷入了困境。因此,系统中有如此多的部分是过度设计的垃圾,这些垃圾是为了填充几年前离职的开发人员的简历而编写的。我在从事的每一份工作中都见过这种情况。每次有人建议尝试最新的库/语言/框架/架构时,都会勃然大怒。
- 这是在一堆垃圾代码之上构建的垃圾代码。错误修复和新功能自然需要越来越长的时间才能完成,这让管理层很不高兴,他们也不知道为什么。他们拒绝承认,他们所培育的快速完成工作的工程文化才是问题所在。
- 代码是由不具备深厚技术栈专业知识的人编写的,充斥着糟糕的架构选择,当时没有人能够识别出这些选择是错误的。我们现在的典型例子就是我们的移动应用程序;它是由一个对移动开发不太了解的人编写的,但他比团队中的任何人都了解移动开发,并且能够构建出基本上可以运行的东西。现在我们有了一位更有经验的移动开发人员,他们对现有的代码库感到震惊。
- 代码实现了模糊或不确定的规范。这种情况通常发生在产品所有者对开发过程中出现的功能问题没有明确答案,而让开发人员自行决定如何工作时。理想情况下,当这种情况发生时,它会被记录下来并在以后重新审视。实际上,它通常不会这样做,而且以后没有人知道为什么功能 X 会这样工作。这类问题更难解决,因为你通常甚至不知道它的存在,直到出现问题。
- 代码库使用了不再支持或已失效的库。债务就是替换它们。
- 项目没有测试覆盖。债务是重新理解系统并编写它们。
- 代码风格规则已经改变,现在有成千上万的警告。债务就是清除它们。
- 代码审查是手动进行的,但永远不会被标记
- 尽管我们恳求 SWE 雇佣更多员工,但他们总是很懒惰,没有充分发挥自己的潜力。除非每个人都通过行动证明自己一直在竭尽全力工作,否则他们不会通过雇佣更多 SWE 来奖励这种懒惰。
2.3 作为一个dev test
- 自动化测试在大多数领域都是肤浅的,但项目可以勾选“测试”已完成。
- 100 亿行 Perl 代码没有编码标准。没有单元测试、没有回归测试、没有测试数据,只有每天流经其中的生产数据。
- 我现在正在看一个 4k 行脚本,它是由一位老 C 程序员编写的(我猜是这样的,因为脚本中使用的所有 40 个变量都在文件顶部声明),我想清理一下。但是我只有脚本处理的 48 种不同数据格式中的大约 15 种的测试数据。
- 我开始清理脚本,使用一些支持脚本自动将输出与现有脚本进行比较。但挖掘或伪造测试数据需要很长时间。我认为生命太短暂了。
2.4 作为一个PM
- 总是有事情发生,但你只能插手一些事情来让你的老板高兴只要让它发挥作用并向前发展,因为这会让管理层高兴。然后,当僵化的架构出现问题时,解决方案就是将其塞进去,因为没有时间进行重构。你第一次就应该做得更好,但第一次做得更好意味着需要更多时间,而这没有得到批准。
- 由于销售人员不断承诺我们无法提供的功能,并且由于需要提供新网站,网站遭受同样的错误和持续的时间压力,因此没有客户获得一个正常运行的网站。
- FDA 提交的申请中有很多勾选工作,并承诺以后会做得更好
- 承诺later要做的事情 可是再也不会来了
2.5 作为一个运维
- 带有纯文本密码的系统,与自定义的基于 SVN 的部署和代码审查系统集成,用于处理开发环境的设置
- 如果你想创建一个新的“分支”,然后在队列中等待你的“分支”一次创建一个(本质上是复制 粘贴和创建新的 Apache 虚拟主机) - 大约需要 20/30 分钟
- 这个分支是在一台旧的 Debian 机器上创建的,这台机器太旧了,无法使用 SSL,因为它已经过时了
- 我们必须进行与第三方 API 交互的工作,其中包括使用脚本将你的工作复制到可以使用 HTTPS/SSL 的较新的 Debian 机器(因为主开发机器已经过时了),然后再返回到旧机器提交更改。
- 无法在本地运行任何代码。您必须使用 SFTP 或已安装的 SSHFS 驱动器编写所有内容,然后在每次更改代码后重新启动 Apache。没有花哨的断点或任何东西,只有 Apache 记录大量“console.log”样式的代码。
- 没有开发/测试数据库。开发是使用所有开发人员都可以直接访问的生产数据库进行的。
- 解决合并冲突祝你好运
- 持续的内存泄漏无法发现,因此我们每 3 天重启一次服务。
- 需要从 Java 8 迁移到 Java 17。Spring 版本较旧。Swagger 较旧。使用另一个 DBMS。Jenkins 管道在过去 2 年中未发生过变化。拆除 10 年前用 NodeJS 构建的遗留系统。
- 所有数据科学都在一个巨大的 conda 环境中运行。请勿安装或更新任何东西!这可能会拖垮整个团队!最终通过为每个项目引入 envs 解决了这个问题。
- 很多机密都提交到了内部(bitbucket。有些时候,一些开发人员被告知不要以明文形式保存密码,所以现在我们在 repo 中也有 base16 编码的机密,这些机密和明文一样糟糕,但也无法通过 grep 找到。
- 代码片段从一个仓库复制粘贴到另一个仓库。如果你在其中一个仓库中发现错误,你必须在修复错误和在同一事物的所有版本之间引入偏差之间找到平衡。由于政治原因,修复所有仓库很难做到。
- 出于某种奇怪的原因,我们的一个系统每次月份更改时都会崩溃,至今没有人弄清楚为什么会发生这种情况。这项服务并不重要,没有它生产也能完美运行,但仍然......
- passwd每台服务器都安装了 KDE。他通过终端会话运行 VNC,然后使用 Gedit 进行系统更改。包括直接在文件 中手动输入用户和密码
- 没有 AD/LDAP。所有用户的机器上都有本地管理员。Azure 仅用于 MS Teams 和 Outlook。在员工离职或数据泄露的情况下,无法远程禁用机器
- 没有本地 DNS。所有机器都使用 /etc/hosts,经wc -l检查发现,该文件目前长度超过 350 行。他的回复是“DNS 在 Solaris 2.6 上不起作用,所以我们不使用它”(我知道这完全是胡言乱语,但这就是他给出的回复)
- 每个用户(包括我自己)都有一个巨大的船锚“游戏笔记本电脑”,因为“这是让 3 个屏幕工作的唯一方法”
- 实际上,没有一台服务器是正确安装到机架上的。每台服务器都安装在机架上的架子上。操作服务器需要将它们从机架上移出,然后放在服务器机房中冰箱大小的变压器上进行操作
- 每台服务器都运行着一些过时的 Fedora 版本。据说是因为引用“我必须合并 Fedora 32/33/34 才能让 Emacs 正常工作”
2.6 作为一个Technical Writer
- 文档是在最后一秒完成的
2.7作为一个老板
- 贫民窟开发:我们接手的产品已经有好几年没有核心团队了,只有一群加入 6-12 个月就离开的初级开发人员。因此,功能就像贫民窟里的棚屋一样被不断添加,没有中央规划或质量控制,代码中充斥着各种半成品功能、半成品重构、不同的标准和风格、重复的依赖关系等。
- 市场驱动开发:更新我们的依赖项以确保我们没有明显的安全漏洞?当市场营销需要立即更改主页图片时,为什么要这样做呢?值得庆幸的是,我们现在已经掌握了这一点,但遗留问题仍然存在。
- 所有技术债务都归结为违反“易于更改”的基本原则。技术债务使系统难以更改,因此,当你小心翼翼地处理它时,你的速度就会减慢。
三 衡量技术债务指标:
技术债务是指以快速而粗暴的方式做某事,而不是以易于测试的精心设计的方式做事。快速而粗暴的解决方案现在很便宜,但日后会花费你很多时间,因此会造成“债务”。
跟踪这样的指标。这就是为什么工程师们会选择这种快速而粗糙的解决方案,因为 1) 这让他们看起来不错,可以快速完成工作 2) 没有人会因为后来出现的问题责怪他们 3) 即使他们受到指责,他们也经常将其推给 QA。4) 有时,如果公司说服客户支付额外的人力来处理代码质量差的问题,那么这个问题就变得毫无意义了,这样公司就可以逃避代码质量差的问题。
如果有一个指标可以跟踪,那很可能就是缺陷的数量,而工程部门负责人将负责决定做这样的事情是否会减少缺陷。
- 新开发人员入职指标 - 新员工需要多长时间才能理解代码库的这一部分。是否有更好的实现方式可以让不熟悉的开发人员更容易理解这一点?如果新开发人员在职责和依赖关系方面无法或不能轻松理解代码库的某一部分,那么很可能是出了问题或以不合适的方式实现的。重构还有一条直接的主线 - “我们的入职培训受到了这段代码的负面影响,我们真的愿意向每位新员工支付每小时 100 美元的费用,让他们花一两周的时间来理解这段代码,还是愿意花 4 到 12 个小时来改进它?”
- 误解指标 - 开发人员在讨论系统的一部分时沟通错误的频率是多少?我曾经在一个系统中工作过,其中“模型”、“控制器”、“库”和“服务”都没有实际定义,因为它们都在文件名和目录中用于执行基本相同的操作。控制器有时会访问数据库。模型有时会定义 api 端点。库有时只是我们 ORM 的一个类。这是一场该死的噩梦,因为没人能弄清楚哪个做了什么,更不用说谈论它了。在这种情况下,我们不得不提出重构,因为我们估计开发人员每周会损失 4-8 小时的工作时间来沟通需要更改的位置和方式。
- “什么时候会爆发”这个指标——实施的局限性是否会导致业务失败?我在另一个系统中工作,做置换计算。它也受到“完成工作就是完成工作”思维的影响。然后,我们想从 6 个条目的排列变成数万个条目的排列。在一个月内。是的,那次会议很有趣,解释了执行时间只有 1 分钟的逻辑很快就会变成以天为单位的工作。我现在更清楚了。我向我们的产品团队清楚地说明了我们系统的局限性,并理解如果我们达到这些限制,这些局限性可能需要修复或以其他方式重构。