还在纠结"技术债务"?这篇文章大胆挑战该隐喻,指出软件开发的本质是持续"权衡"而非欠债。学习聚焦决策的真实后果与业务影响,做出更明智、更重价值的选择,告别空洞的"还债"口号。
欢迎来到新的一周!
刚进房间时,我便情不自禁地笑了出来。在电话会议中,我的朋友 Arek 对某人说:"我们会把它加到技术债里。" 那是几年前的事了,我大概也是第一次听到这个词。那时的我正值爱讽刺的年纪,觉得这简直是个笑话。但 Arek 并非在调侃,而是在计划会议上认真提出了这一说法。
技术债(Technical Debt)。再次品读,是否觉得有些啼笑皆非?
这个词在我们的行业里被频繁使用。我们在会议上提到它,在代码审查时讨论它,还用它来为代码重构工作辩护。但我认为,大多数人理解的"技术债"实际上并不存在。它只是一个方便的标签,让我们能够承认问题,却不必真正去解决它们。在今天的文章中,我将尝试为我这个大胆的观点辩护。
技术债的幻觉
我们都曾身陷囹圄:时间紧迫,管理层施压,为了尽快完成任务,总有人抵挡不住为赶工而忽视质量的诱惑。Ward Cunningham 在 1992 年的 OOPSLA 会议上首次提出了"技术债"这个概念。他是这样说的:
另一个更严重的陷阱是未能进行整合。尽管不成熟的代码可能运行良好,并且完全可以被客户接受,但过多的数量会使程序变得难以掌控,导致程序员的过度专业化,并最终导致产品缺乏灵活性。首次发布代码就像欠债一样。只要及时通过重写来偿还,少量的债务可以加快开发速度。 对象使这种交易的成本可以承受。危险发生在债务没有偿还时。 花在不太正确的代码上的每一分钟都算作该债务的利息。整个工程组织可能会因未合并的实现(面向对象或其他)的债务负担而陷入停顿。
技术债的经典观点认为,我们可以现在走捷径,然后在以后"偿还",就像从未来借时间一样。但是,一旦代码被编写并发布,它就成为我们现实的一部分。没有一个神奇的账户,我们可以将未来的工作存入其中来偿还债务。
兰尼斯特家族有债必偿,开发者也是如此,对吧?理想很丰满,现实很骨感,但开发者真的能像兰尼斯特家族一样有债必偿吗?让我们看下面这张图。
想象一下,一个团队正在努力交付一项新功能。为了赶上截止日期,他们省略了单元测试的编写,并承诺日后补上。该功能按时发布了,但几个月过去了,测试却始终没有编写。这是个问题吗?
有些人可能会耸耸肩说:"当然,这就是技术债呗。"
但为什么这是个问题?
或者给你一个提示:什么时候它会成为一个问题?
为赶工而忽视质量显然会增加出现 bug 和安全问题的可能性。当我们在没有测试的情况下发现这些问题时,我们将更难诊断和修复它们。
但如果我告诉你,这个团队正在:
- 构建一个概念验证(proof of concept),
- 交付一个支持流程的内部工具,
- 设置一个一次性的用户报告工具,
- 升级遗留系统中的依赖项,
- 对旧系统进行几年来第一次更改。
这会改变你的看法吗?你还会称之为技术债吗?
为赶工而忽视质量的程度可能相同,但后果的严重程度却可能大相径庭。技术债本身并不是问题,我们决策的后果才可能成为问题。
正是通过 将缺少测试简单地归类为技术债,我们才推迟了对真正问题的关注。我们没有承认我们做出了权衡:速度优先于质量。这个术语变成了一种拐杖,让我们在没有解决根本问题的情况下感觉良好。这是一个很容易找的借口。
"我只是把它放在这里,和剩下的火一起。"
这种场景我已屡见不鲜,对此我深感厌倦。
技术债的归宿,我们心知肚明——Jira 待办事项列表的黑暗角落。
权衡取舍:软件开发的现实
软件开发,实则是一场又一场的权衡取舍。每一个决定都牵一发而动全身,有些后果立竿见影,有些则潜移默化。而将这些决策简单地归结为"技术债",无疑是对复杂现实的过度简化,也掩盖了我们真正面临的选择。
在讨论技术债时,我常听到一种论调:降低质量必然导致更高的维护成本。 我在《可移除性优于可维护性》一文中写道,可维护性在我们的行业中被视为一个终极目标。
让我告诉你一件事:你的系统可能永远不会达到可维护性至关重要的阶段。
为什么?不幸的是,我们的大多数系统要么是略微成功,要么是根本不成功。就我个人而言,我曾参与过一些系统的构建,这些系统构建了几年,甚至没有真正的生产用途。不幸的是,浪费时间是我们工作中一个很大的问题。我们浪费时间不是因为编码速度太慢,而是因为我们在做无关紧要的事情。此外,关于维护代码,它应该被直接删除或系统被重写。
我们大多数人都不愿意在遗留系统中工作。 它们在我们看来名声不好。它们被视为过时、笨重且需要更换。但是这些系统已经过实战检验。它们随着时间的推移而发展,可以处理新系统可能无法预料的真实场景。
有时,我们对多人编写的 10 年前的代码感到厌恶。我们不想接触那些过时的技术栈。我们说技术债太高了,我们需要重写它,而且大多数时候,我们都想着重写整个系统。当我们的管理层要求我们提供理由时,我们会尝试重复这个听起来很聪明的说法。然后,我们对残酷的现实感到沮丧。管理层说不。为什么?
权衡分析清单
通常,我们不做功课,也不检查旧代码和旧技术栈本身是否是一个问题。我们只是想做出改变,让我们的生活更轻松。但遗憾的是,这站不住脚。
我说的功课是什么意思?例如,回答以下清单:
- 代码变更频率如何?
- 上次代码修改是何时?
- 本次修改预计耗时多久?
- 修改时间差异显著吗?
- 为什么差异这么大?
- 是否有其他人更熟悉这段代码,可以进行更改?
- 我们可以一起做吗?
- 更改的实际成本是多少?
- 如果我在这段代码中犯了一个错误,最坏的情况会是什么?
- 这个错误会造成多大的损失?
- 还有哪些其他潜在问题?
- 它们发生的可能性有多大?
- 类似问题是否也存在于其他功能?
- 我们如何解决这些问题?
- 是否有其他解决方案?
- 最快速的解决方案是什么?
- 最具成本效益的方案是什么?
- 我们可以不做这个更改吗?
- 人力成本 vs. 自动化成本?
- 如果我们进行此更改,会产生什么影响?
- 如果我们不进行此更改,会产生什么影响?
- 我们如何衡量它?
- 等等。
这才是我们应该问自己、我们的业务部门和我们的同事的真正的分析和问题。通过收集答案,我们可以做出有充分理由的明智决定。
这才像话嘛!
在提供了这个分析的结果之后,而不是像下面这样空洞的陈述,我们才能听到业务部门这样说:
变更成本(即代码维护和重构的代价)
Ward Cunningham 提出"技术债"这个概念,初衷是为了让非技术人员也能理解那些半吊子代码的代价。 它本意是一个隐喻,而不是一种会计方法。但是,当我们开始将其视为可以像管理财务债务一样进行量化的债务时,我们就会遇到问题。
与财务债务不同,代码不会随着时间的推移而自行恶化。它不会累积利息。挑战出现在我们需要更改或扩展该代码时。困难不是时间的函数,而是上下文的函数——新的需求、不同的团队成员、不断演变的理解。
话说回来,Ward Cunningham 提出"技术债"这个概念,那可是 1992 年的事儿! 32 年前啊!那会儿我才 7 岁,Boyz II Men 的"End Of The Road"火遍全球。这歌儿现在都没人听了,可技术债却一直流传到现在,也是神奇!
那个年代,数字化转型和自动化是主流,业务和 IT 泾渭分明。 所以,"技术债"这个比喻,在当时或许还能勉强弥合理解上的鸿沟。但今时不同往日,IT 本身已成为业务的核心组成部分,我们亟需更精准的语言来描述和解决问题。
一个遗留系统,仅仅因为"难用"就被打上"技术债"的标签,这公平吗?也许它稳定可靠,完美地实现了最初的目标。真正的挑战在于,如何在不完全理解其内在复杂性的前提下,让它适应新的需求。而我们不应该仅凭直觉或感觉来做判断。
可能出现以下情况:
- 编写一个程序让某人手动执行任务更便宜,
- 取消该功能并以不同的方式执行,
- 进行一次性更新,即使更改成本很高,但它每年发生一次,并且重写成本会高得多。
也可能出现我们需要重写整个功能而不是添加另一个复选框。 我就遇到过这种情况。我当时在开发一个员工管理系统。我负责处理雇用和处理雇佣合同的法律部分。我必须添加一个复选框。然而,事实证明:
- 文档稀疏且分布在各处,
- 我没有找到创建它的任何原始开发人员,
- 我没有找到任何可以向我解释我负责的流程的现任员工,
- 我做了关于理解和潜在后果的笔记。业务部门没有人可以确认或否认我是否正确理解了这一点。
- 错误更改和法律后果的风险非常严重,以至于我们无法承担出错的风险。
我坦言,我可以按照字面意思进行"简单"的修改,但由此产生的后果,我概不负责。 业务部门自然也不愿背锅。于是,我提出了一个替代方案:以最小的范围进行重点重构。我详细阐述了计划,从深入分析入手,并在后续过程中与业务部门紧密协作,不断验证我的假设。当然,我之前在业务部门工作过的经历也为我赢得了他们的信任。最终,我将选择权交给了他们,而他们也明智地选择了重构。
这次重构之所以成功,绝非是因为我们消灭了技术债,而是源于明确的业务需求,以及基于精确分析和充分权衡取舍的协同合作。
我们应该承认现有系统的价值,然后选择逐步重构和现代化其中的一部分,而不是完全替换它。我从未见过尝试 1:1 设计新系统的成功重写。
我们应该牢记 帕累托原则,聚焦于那 20% 能够创造 80% 业务价值的核心功能。 这才是重中之重,远胜于空谈"技术债"。
沉没成本谬误
Seth Godin 在他的文章《忽略沉没成本》中写道:
你有 Springsteen 演唱会的门票。它们真的很难获得。你花了四个小时在 StubHub 上搜索,直到你找到了每张 55 美元的完美座位。
在你进入会场的路上,有人提出以每张 500 美元现金的价格购买你的门票。你应该卖掉吗?
事实证明,你花在购买门票上的时间量无关紧要。如果你不愿意为这些门票支付 500 美元(而你没有,否则你就会这样做),那么你应该愿意以 500 美元的价格出售它们。花 250 美元吃晚餐,然后去购买明天晚上演出的更好的门票。
或者说你犯了一个错误,去参加了演唱会而不是出售(这些座位现在价值 500 美元)。但是 Bruce 生病了,Manfred Mann 代替了他。你不太喜欢他。但是你花了 500 美元购买了这些座位!你应该留下吗?
当我们在做出决策时,即使当前的成本超过了收益,我们仍然会根据累积的先前投资(时间、金钱或精力)继续投资,这就是沉没成本谬误。
Seth 写道:
在两个选项之间做出选择时,只考虑未来会发生什么,而不是你过去做出的哪些投资。过去的投资已经结束、损失、永远消失了。它们与未来无关。
所以,归根结底,技术债不过是一种幻觉,它并不真实存在。
当下,你所拥有的一切——代码、技术、流程、团队——才是真实存在的。而过往的一切,都不过是沉没成本谬误,是你对过去所作所为的记忆罢了。
理解过去的情况至关重要;这就是为什么我们应该编写架构决策记录。但是,我们应该根据当前状态做出决策,因为那是我们的现实。
衡量虚拟债务是在浪费时间。我们不应该专注于对过去进行分类和衡量。我们应该衡量和优化实际影响,例如交付速度、结果等。
说白了,所谓的"技术债",不过是待办事项清单上的一项,以及人们为自己摸鱼和处理烂代码找的借口罢了。
甚至"低质量代码"是什么意思? 你可能有糟糕的代码;如果没有必要更改它,它就不会对交付产生负面影响。
对我来说,谈论技术债是一种替代性对话,它让我们感觉良好,因为"我们正在行动",而不是具体谈论我们的流程有什么问题。
但是,如果我们的流程存在缺陷,我们应该改进它,不是因为我们有"技术债",而是因为,例如,我们的交付速度较慢或质量较低,这会转化为财务问题。
令人欣慰的是,这种局面正在逐渐改变,越来越多的团队开始重视记录设计决策。然而,核心问题依然存在:我们往往忽略了那些被放弃的备选方案,更重要的是,我们从未在决策中设定"有效期"或"失效参数"。
例如,我们可以这样记录:
假设我们决定采用 NoSQL 数据库来存储用户信息,可以这样记录:'我们假设,如果用户数量超过 100 万,且查询量超过每秒 1000 次,NoSQL 数据库的性能将无法满足需求,届时需要考虑切换到关系型数据库。请在用户数量接近 100 万或查询量接近每秒 1000 次时,重新评估此决策。'
多亏了这一点,我们为未来的自己提供了关于何时评估我们过去假设的提示。决策永远没有好坏之分,但决策的结果可能是这样的。
诚然,记录在案并不能保证万无一失,但至少能让我们在未来的讨论中,基于翔实的数据,而非泛泛而谈的"技术债"(即我们当初未曾做好的事情),共同探讨未来的方向。
与利益相关者沟通
对非技术人员来说,"解决技术债"毫无意义。 他们只关心结果:性能、稳定性、用户体验,而非代码是否优雅。
不要说:
我们需要重构这个模块,因为存在技术债。
而是从影响的角度来描述:
改进此功能将减少 50% 的页面加载时间,从而增强用户体验并提高用户留存率。
或者:
通过优化我们的数据库查询,我们可以节省基础设施成本并提高可扩展性。
也许有人会反驳:"这些数据我从哪儿来呢?!"答案很简单:与你的同事和业务伙伴沟通,做好充分的调研。一句"技术债"并不能解决问题,逃避责任更是不可取。当然,这绝非易事,但我们责无旁贷。量化收益需要付出努力,收集数据和指标来佐证我们的观点更是必不可少的。但是……
正因如此,利益相关者才能真切地感受到价值所在。你不再仅仅是一个代码匠,而是成为了他们的战略伙伴,为他们提供决策所需的数据支持,将技术专长与业务目标紧密结合,真正为他们分忧解难。正如我们在与 Gojko Adzic 的网络研讨会上所讨论的那样:
有效的沟通弥合了技术团队和业务利益相关者之间的差距,确保每个人都对优先级保持一致。
持续改进
与其打着"技术债"的幌子一拖再拖,不如将改进融入日常工作流程中。
你是否也曾幻想过"重构冲刺"?或是雄心勃勃地计划一场"零架构冲刺"?
我们应该尝试在日常开发中进行小的、持续的改进,积少成多,避免日后的大规模重构,让代码库始终保持健康。这些看似微小的改变,日积月累,终将塑造你的团队文化,产生深远的影响。强烈推荐 James Clear 的《原子习惯》一书,相信会对你有所启发。
始终牢记业务价值。关注重要的地方。聪明地工作,而不是努力地工作。正如我之前提到的,这将有助于你与业务人员建立信任。
技术债已死;它真的死了
最后,让我们再次回到 Ward Cunningham 本人,看看他是如何重新解读"技术债"这个比喻的:
我从不提倡故意编写劣质代码,但我支持编写能够反映你当前对问题的理解的代码,即使这种理解并不全面。
你知道,如果你想通过开发你没有完全理解的软件来以这种方式承担债务,那么你明智的做法是使该软件尽可能地反映你的理解,以便在进行重构时,清楚你编写它时的想法,从而更容易将其重构为你现在的想法。
换句话说,整个债务比喻,让我们说,偿还债务的能力,并使债务比喻对你有利,这取决于你编写的代码是否足够干净,以便在你理解问题时进行重构。
因此,换句话说,技术债并非是为追求速度而牺牲质量的权宜之计,而是基于我们对当下情境的最佳理解所做出的决策。即使这种理解存在局限性,我们也应该坦然接受。
不过,重点是未来。只有在我们做出选择的情况下,我们才能看到我们过去选择的结果;更重要的是,如果我们不需要根据它们做出另一个决定,那么它们将是不相关的。
所以请,请,请……
停止滥用"技术债"这个被用烂了的比喻吧!
行动起来吧!做好功课,接受现状,抛开沉没成本的束缚,通过严谨的分析,用充分的业务论据来说服你的同事,共同改进我们的设计!
没有简单的修复方法或神奇的公式。它需要持续的努力、清晰和直接的沟通,以及面对挑战的意愿。但是通过这样做,我们构建更好的软件并交付价值。