Hacker News 中文摘要

RSS订阅

代码之烂没有下限 -- There's No Limit to How Bad Code Can Get

扎克·凯斯(Zach Kehs)十多年前第一次接触到丑陋的遗留代码库,当时他刚从大学毕业,以软件开发工程师的身份加入亚马逊(Amazon),所在的团队负责维护与某业务相关的代码。

正文

1:登上正在沉没的船

十多年前,我第一次接触到恶性遗留代码库。那时我刚从大学毕业,以“软件开发工程师”的身份加入亚马逊,在一个负责订单处理的团队工作。从表面看,我们的代码要做的事很简单:处理订单时往数据库写点数据,再调用其他团队的服务,要么是确认有效性,要么是更新对方那里的记录。我和同事估算,这个系统只要二十来个精干的工程师就能维护和迭代。可实际上,这个组织有几百号人,系统已经膨胀到没人能搞懂全貌的地步。

在这个组织里待超过几年的人很少, institutional knowledge(机构知识)早已流失。代码里满是“闹鬼的墓地”——也就是没人敢碰、也没人能讲清来龙去脉的旧模块。修改系统的动力本就不足,恐惧更是压垮了任何简化现有系统的尝试。各类订单该走什么业务规则,都是很久以前、早已不在公司的人定的。这些规则偶尔能在一份自诩为“活文档”却早已过时的文件里找到,更多时候根本无处可查。想自己追查逻辑也不容易,因为大量代码散落在其他团队,根本访问不到。

要是某个订单的隐蔽流程没跑起来,寻呼机就会狂响,提醒我们依赖的服务链里有人在闹脾气。靠着这种反馈机制,系统勉强维持运行,但改起来极其困难,性能也惨不忍睹。即便如此,为了支持亚马逊最新的产品和功能,新的层级还在不断往上加。这种状态让人觉得不可持续,也正是这种感觉,让我和很多人用“沉船”来比喻那些不思偿还技术债的组织。

话说回来,也一直有人在尝试修复架构,流程通常是这样的:新经理或资深工程师加入,发现“情况很糟”。领导层也认同,也想改善,但抽不出工程师,所以每次修复都变成了增派人手和新团队。

这些重构无一成功:系统需要好几年才能吃透,而且还在不停变化。花这么久去设计修复方案在政治上根本行不通,所以所有人都只能在不完整的信息下工作。部分急躁也来自内部:如果你要设计一个宏大的修复方案,多少也是为了升职,自然不想等太久。

每一轮最终都以新方案的残骸永久嫁接在架构上告终,牵头工程师拿着升职离开了。增加的人手留了下来,因为迁移计划太痛苦、太不得人心,根本没法彻底完成。这个循环在我加入之前就在转,在我离开之后还在继续。那艘沉船似乎永远沉不到底。

2:它到底会怎么结束?

离开大约三年后,我和一位前同事通了电话。他在那待了六年,刚离职不久,又见证了一轮完整的循环。我们都在倒苦水,尽管离职时间差了好几年,却都不约而同地用“沉船”来形容那个组织。

“那最后会怎样?怎么收场?”他问我,想听听我对那个组织未来的看法。

这个问题和比喻让我觉得不对劲,我意识到它混淆了两件事:我们说的是代码,还是公司?

公司是会沉的。劣质软件确实拖累业务,但影响有多大,取决于很多因素。像亚马逊这种现金流充裕的公司,能容忍内部一定程度的腐化,不会立刻伤及底线。换了另一家对软件质量更敏感的公司,劣质软件可能就是在给竞争对手递刀子,等着对方把你的船底凿穿。而且,大语言模型也改变不了这个事实。

至于代码,沉没是没有终点的。那是一艘“无限沉没”的船,因为代码能烂到什么地步,没有下限。你并没有逃出一座即将倒塌的大楼,而是处在一种持续、永不终结的坍塌状态中。用暗示终点的词汇来描述它,本身就是错的。

软件属于抽象领域,不像建筑或桥梁那样落在物理世界,看得见、摸得着。你要是没完没了地给大楼加盖楼层和房间,它迟早会塌。软件没有这种约束。代码永远可能变得更糟,永远可以再套一层间接调用,再降一点性能。

爱较真的人会指出,代码烂到一定程度就会彻底跑不起来。但在实际中,这类破坏性变更很快就会被回滚。而此前那上千次让代码变烂的改动,并不会被撤销。软件会继续保持“能跑”的状态。真正没有可回滚的破坏性变更的情况是:劣质软件的成本飙升到超过收益,或者开发速度趋近于零,因为任何改动都会搞出故障。在所有这些情况下,都是公司先死,而代码远没到达什么“下限”。所以,别假装那个下限存在。

3:技术债没有破产保护

劣质软件对业务的拖累是真实威胁,这也是优秀组织重视代码质量的原因。既然软件质量没有突如其来的故障阈值,人们常把它比作“技术债”。这个比喻比“沉船”好一些,因为债务可以无限复利累积,但它同样不完美。

债务有一个“终点”,因为破产是强制重置。而软件领域的对等物——彻底重写——几乎从来都不是一个选项。

像亚马逊这种巨无霸企业,最接近的做法是我所说的“侧路”:拆分出一个团队,针对某个新用例,只保留最精简的功能,从零搭建一套完全独立的新系统。往后,新的用例可以选择导流到这个简化版系统。关键的是,旧系统必须继续存在并维护——这不是“终点”,因为所有旧用例还在,未来到底该用哪个系统,只会给组织带来新的痛苦。这根本不是我们想象中破产那种一笔勾销。

错误的软件心智模型会导致糟糕的决策。如果存在一个“硬重置”的逃生舱,那么把技术债往后推就显得没那么严重。相信“反正很快就能重写”的错觉,会让你今天做出更差的决定,因为今天的决策者根本不明白:根本就没有逃生舱。

“倒塌的大楼”或“沉没的船”这类比喻并不适合软件,但我们可以借用它们来强调软件的特殊性:这栋大楼是在无限坍塌,这艘船是在无限沉没。没有任何自然约束会拍醒你的项目经理,逼他去处理技术债。软件只有靠我们主动努力去阻止沉没,才能保持高质量。去拿个桶吧。

评论

大量评论者都在用自己的“烂代码遭遇战”佐证同一个现象:企业软件里“能跑就行”才是常态。真正的分歧在于两条线:技术债到底能不能像破产一样清算,以及 LLM 究竟是清债神器还是新的造债机器。

烂代码是行业常态,“能跑就行”才是硬标准

  • 低成本外包、电信内部系统、上世纪的 FORTRAN 维护现场,到处是几万行单文件、六到十层嵌套、单例滥用、没注释没函数的巨型代码;这不是个别公司的毛病,而是维护工程师每天要面对的地貌(较多人) > 社区天天讲抽象数据类型、以结果为先、代码卫生,但现实里大量代码只是为“打完卡下班”写的,最常听到的是“它能跑,那就别动”(@jdw64)

烂代码的根子是截止日期和沟通成本,不是一句“程序员不行”能解释的

  • 合同交付以赶截止日期为核心,离岸外包的议价权又天然压在成本上,封装自然被权力层级打碎;开源项目之所以好很多,是因为代码直接代表作者声誉(较多人)
  • 长期项目的规则与假设只有一半被写下来,跨平台产品的 API 和操作系统还在不停变化,团队没法停下重画蓝图,只能在航行中补船;嘴上说“下次重构”的那次往往永远不会来(较多人) > 不能停住这个世界重新做架构,只能边发版边还债,一些老路径往往要等五到十年才能彻底拆掉(@socalgal2)

现代 LLM 是收拾这类烂代码的趁手工具

  • 现代大模型能带着完整上下文钻进去找边界情况、补测试、搭环境并一步步执行重构;电信公司那个需要手工解一个月的绳结,用 AI 也许一天就能解完(较多人)
  • 更进一步有人认为,以当前模型的“审美”,想让它写回那种超泥球都难;这种泥球恰恰是人在通用化重构太昂贵的现实下被逼出来的产物(少数人) > 所谓超泥球是人类的特产:对又脆又层层加码的代码做泛化重构,成本高到没人愿意付,于是只能继续堆(@hombre_fatal)

指望 LLM 一键重构烂代码,只会得到更大的烂摊子

  • 少数人冷笑:如今的“重构”会被包装成一次成功的 LLM 大改造,结果多半是把船凿得更沉,然后回社交网络宣布胜利;要是代码本身已经接近反汇编、混淆代码,模型也无从下口(少数人) > 如今这帮人大概会让 LLM 把代码弄得更糟糕,然后回来晒一堆胜利(@boguscoder)

技术债也有“破产”:替换、退役,乃至沉船

  • 想结束一笔技术债,不一定要“还清”:可以直接替换掉旧代码、买第三方方案、把业务流程改到不再需要这段逻辑,或者干脆让系统退役;这就像宣布破产,短命系统甚至可以故意欠债直到关门,而长命系统才需要按时还本付息(较多人)
  • 代码若烂到没人敢继续运营,它的终点不是无限恶化,而是像沉船一样停在那里,随后慢慢软件腐烂,不再对企业造成实际影响(较多人) > 技术债的破产选项确实存在——停止使用那部分代码或技术,用重写、外购或流程改造把它替换掉(@blevinstein)

有价值的补充

  • 贝佐斯当年的“服务接口强制令”是康威定律的经典应用:当系统大到没人能全部理解时,强制团队把功能和数据暴露成接口、只能通过接口互相通信,等于用组织边界切断复杂依赖(@adolph)
  • 删复杂度和加复杂度完全不对等:移除一层复杂度至少要比当初添加它难两倍,上不封顶。这种非对称性才是“下一次重构”永远没有下文的根本原因(@jeberle)
  • 一个非程序员视角的底线提醒:坏代码如果还能产生想要的结果,至少还算“有债的资产”;一旦烂到根本产不出结果或者不再是人们想要的结果,它就成了无价值的纯文本,这才是技术债真正失控的样子(@linuxrebe1)