程序员对重构的执着与现实的割裂
程序员普遍喜欢重构(Refactoring: 在不改变代码外部行为的前提下,改进其内部结构的过程),因为将混乱的代码变得整洁能带来极大的愉悦感。在程序员圈子里,重构被公认为一个好习惯,无论是学校里的老师、职场中的前辈,还是各种软件开发理念和编程思维,都经常提及重构。
然而,理想很丰满,现实很骨感。经验丰富的开发者会发现,项目真正开始后,进行大规模重构的机会少之又少。大多数时候,重构仅限于在提交PR(Pull Request: 软件开发中,开发者请求将自己分支的代码合并到主分支的操作)时,顺便做一些变量重命名、拆分函数之类的小修小补。将重构作为一个有分量的任务来对待都很少见,更不用说将其作为某个开发周期的主要工作了。程序员非常重视重构,但项目组却不以为然,这种割裂的状态究竟是如何造成的呢?
重构理论的起源与TDD的兴衰
重构的故事要从头说起。作为90年代软件开发理念的“墓志铭三部曲”中的最后一部,重构是唯一一个源自学术界的理论。1990年,来自伊利诺伊大学的在读博士生William发表论文,首次提到了“refactoring”这个词。一年后,华盛顿大学的另一位博士生发表的博士学位论文,首次全面系统地构建了重构理论,主要针对主流的Functional Programming(函数式编程: 一种编程范式,将计算视为数学函数的求值,并避免状态和可变数据)和Procedural Programming(过程式编程: 一种编程范式,通过调用过程(子程序、函数)来组织代码,强调步骤和顺序)代码。又过了一年,William自己也发表了博士学位论文,他构建的则是面向OOP(Object-Oriented Programming: 面向对象编程: 一种编程范式,以“对象”为基本单元,将数据和操作数据的方法封装在一起)代码的重构理论,因为他的导师正是OOP圣经《Design Patterns》的四位作者之一。
重构这种做法,能从实验室里的理论走进项目组里的实践,多多少少要归功于“墓志铭三部曲”中上一期主角——TDD(Test-Driven Development: 测试驱动开发: 一种软件开发方法,要求在编写任何生产代码之前先编写自动化测试)。作为Agile(敏捷开发: 一系列软件开发方法,强调迭代、增量和适应性,以快速响应变化)体系的基石,Extreme Programming(极限编程: 敏捷开发的一种,强调频繁发布、测试驱动开发、结对编程等实践)模式中,TDD是最深入日常工作的指导方针。TDD的三个主要步骤——写测试、写代码、重构代码——环环相扣,缺一不可。因为在TDD的写法里,如果代码不重构,屎山(Spaghetti Code: 意大利面条式代码: 形容代码结构混乱、逻辑复杂、难以维护的糟糕代码)会呈指数级上升,很快就会举步维艰。因此,在Agile体系全盘接管开发的年代,重构无疑是不可或缺的一环。
然而,成也TDD,败也TDD。随着Agile开发被大家做成“Agile风味”的开发,捆绑销售的TDD也不再受宠。脱离了TDD的重构,也开始变得可有可无。对于这个成本不小、价值不明的工序,负责管理项目的人们开始提出那个致命的问题:“我们为什么要重构来着?”
重构的商业价值困境
这个问题最标准的回答来自于重构的圣经——Martin Fowler的名著《refactoring》。根据这本书的解释,重构是为了让代码“easier to understand and cheaper to modify”(更容易理解,修改成本更低)。虽然这里没有提到主语,但我们都知道这句话的主语是程序员。让程序员更容易看懂代码,让程序员可以更轻松地改动代码,从头到尾,重构都是一个以程序员的身心健康为主要考量的理念。而这,也正是让重构在业界寸步难行的根本原因。
在业界,一切行为都可以用价值和成本来衡量。那么一个新产品在开发时,就有两种选择:
第一种是在开发初期就投入精力进行重构,时刻让代码保持在高质量的状态。这会在早期影响产品的商业价值爬坡速度,因为每花一分钟在重构上,就会少一分钟在业务需求的开发上。但随着产品的成熟,持续重构的高质量代码就会发挥优势,更少的屎山和技术债(Technical Debt: 技术债务: 指在软件开发中,为了快速交付而选择的短期解决方案,导致未来需要投入更多成本进行修复和重构),就意味着更快的新功能开发、更少的bug和维护成本。这反映在商业价值上,就是更快的提升速度。
第二种路线则是在开发初期就埋头猛干,使劲造产品,不去管代码质量。这样能让产品更快地发布、更快获得用户,更快占领市场、更早地提升商业价值。但进入后期,就会面临积累下来的屎山代码、即将过期的技术栈、频繁出bug的生产环境。开发团队则需要投入越来越多的维护成本,新功能的开发就会面临更少人力、更高风险的困境,这会极大程度地拖累产品的增值速度。
当然,以上皆为理论。那么在现实中,软件开发会选择哪条道路呢?
鸿沟理论与重构的商业悖论
回答这个问题之前,我们要先复习一下在科技行业堪称真理的“鸿沟理论”(Chasm Theory: 市场营销理论,描述创新产品从早期市场向主流市场推广时面临的巨大障碍)。根据鸿沟理论,任何产品都要先经过早期市场的探索。你的大方向、你的产品卖点,需要先被一个小众的、前卫的群体所接受,证明它是有市场的,然后才可以向大众推广。在这个早期阶段的产品状态,就是大家熟悉的MVP(Minimum Viable Product: 最小可行产品: 指具有最少功能集,足以满足早期用户需求并验证产品理念的产品版本)。
MVP状态的产品,不需要有很高的商业价值、很完整的功能,所以如果我们在路线图上标注MVP的位置,它的海拔会很低。MVP最重要的是速度,因为如果你把时间、精力、预算都花在一个MVP上,最后发现没人接受,那你已经完蛋了,甚至没有翻身的机会。明眼人都能看得出,路线二会比路线一更早地达到MVP状态。这是为什么,有脑子的,在开发早期必然选择路线二。这是从风险和收益的角度来说的最优解。
假设现在我们成功发布了MVP,也获得了早期市场的认可。那么现在是否就可以考虑调整项目方向,开始重构了呢?当然不可以。因为我们现在的任务是跨越鸿沟,进入主流市场。这个理论之所以叫做鸿沟理论,就是因为主流市场和刚才经历的小圈子是完全不同的战场,它们之间的差异如同鸿沟一般。小圈子能容忍残缺的功能,而在主流市场你甚至端不上桌;小圈子有极强的动手能力,但普通人需要喂到嘴边的简单直接;小圈子有什么新鲜事物都会试一下,大众只会跟风用最近最火的东西。所以为了跨越鸿沟,你的MVP需要蜕变成为一个完整的、符合主流印象的产品。而且你是在和时间赛跑,因为在同一时间,无数其他产品也在渡河。到最后,能赢得市场的只有几个头部玩家。这就是主流市场的本质,赢家通吃的零和游戏。你如果在这个时候开始投入精力重构,你就是主动把领先地位让出来。所以在这个阶段,有脑子的也不会改变路线。
假设我们现在已经成功跨越了鸿沟,在主流市场站稳了脚跟,成为了存活的几个头部玩家之一。这个时候是不是终于可以重构了呢?不好意思,还是不行。就像前面提到的,在这个阶段的产品,已经积累了足够多的技术债,重构的成本已经远远高于MVP时期。这个时候的重构,就成为了一个短期成本和长期价值的组合。那么有谁愿意为这个用当前的高风险去赌一个未来的高收益的项目买单呢?尤其是在当今的商业环境下,风险肯定是自己承受的,但收益你不一定能等得来。没有铁饭碗的人,不会想着为别人做嫁衣;有铁饭碗的人,不会没事给自己找风险。所以从商业的角度来说,在软件工程里,永远都没有重构的一席之地。证明完毕。
拓宽重构视野:从狭义到广义
但产品开发和代码重构其实不需要站在对立面。虽然越是成功的产品,就越会有人抱怨屎山,越会让人觉得大厂都是什么草台班子,这是因为成功的产品不代表成功的代码。但反过来,如果想要获得重构的机会,想要获得好代码,我们还真得跟产品搞好关系,毕竟预算是抓在人家手上的。
有些人的做法是把“代码质量”打造成一种KPI(Key Performance Indicator: 关键绩效指标: 衡量组织或个人绩效表现的量化指标),让它和产品交付绑定。比如SonarQube、CodeSense这一类代码分析工具,可以在一定程度上量化代码的质量。这一步是不难的,难的是下一步:怎么把这些量化结果转化成商业语言?你不能直接说“代码质量从90分降到80分了,所以我们要大搞重构”,你得说的是“代码质量从90分降到80分了,造成了XX影响,所以我们要大搞重构”。这个XX得和产品的商业价值挂钩,这很显然是SonarQube做不到的。反正我是没有见过谁能成功地用“代码质量评分”为理由,推动大规模的重构。
这也不代表着重构无望了。要拯救重构,我们首先要打开视野,回到理论的原点。在William的论文里,重构的目标是“preserve the behavior of a program”(保留程序的行为),而在Martin的书里,则是“without changing its observable behavior”(不改变其可观察到的行为)。核心都在于“behavior”这个关键词。这个behavior到底是对谁而言的呢?程序员?项目经理?还是客户?不同参与方的角色定位不同,他们可观察到的behavior也不一样。
基于这个观察,著名程序员Atomic Energy提出了狭义和广义的重构论。其中狭义的重构,就是追求在代码层面,不改动接口、不破坏单元测试,甚至连程序员都不会察觉的改动。这就是最经典的重构。而广义的重构,就是只要用户没发现有什么不同,那就可以了。程序员们追求的狭义重构论,就有点像是在向上级乞讨:“我保证动作会很小,改动会很轻微,不会影响到业务的”,“求求你让我重构一点点吧 😭”。姿态太卑微了!你越是卑微,就越体现不出价值,就越难争取到机会。
我们应该追求的是广义的重构,因为这样才能拓宽我们的行动范围,纳入更多的价值体系、获得更多的筹码。广义重构最有名的一次实践,应该算是10年前阿里巴巴搞出来的“中台架构”(Middle Platform Architecture: 一种企业级架构理念,旨在将企业通用能力沉淀为可复用的服务,以支撑前台业务快速创新)。整个集团里所有业务的所有系统的数据权限被收归到一起,统一管理。这是广义的重构,因为普通消费者不知道这个事情,只是震惊了所有业内人士。虽然最后这个架构灰溜溜地收尾了,但能把这么大规模的重构项目推动起来,就足以载入史册。
我们不需要追求这么极端的效果。简单点,在自己的项目中找到可以挂靠的指标,给重构行为上价值,就已经很有效果了。比如我在【让编程再次伟大#22】里分享的软件开发3M原则,其中后面的两步“make it right”和“make it fast”,本质上都是重构。但它们是师出有名的,因为“right”可以体现在技术栈升级上,旧技术过期所产生的额外维护成本和安全风险都是可量化的。而“fast”就更直观了,尤其是按量计费的云服务用得多的,多快一点就能多省一点。这种把项目经理、产品经理牵涉进来的做法,表面上看是给他们增加了麻烦,实际上,你是给他们送上了一份礼物,一份可以向上汇报的新成果。这种好事,自然就更有机会获得大家的支持。
AI编程时代的重构曙光
不过我觉得,原教旨主义的各位狭义重构论的支持者,也不需要气馁,因为你们起死回生的曙光就在眼前了——这就是现在的AI编程趋势。如果我们把Cursor的发布算作是AI编程时代的开端,到现在满打满算也有两年了。不管程序员们愿不愿意,从每天的新闻里,我们都可以看到各大公司的CEO兴高采烈地宣布,自家的代码现在有百分之几十由AI生成。
与此同时,一个新的问题也随之诞生。因为Transformer模型(Transformer Model: 转换器模型: 一种深度学习模型架构,广泛应用于自然语言处理任务,是许多大型语言模型的基础)的本质是一个统计学模型,所以在大量数据的参考下,它给出的做法必然是趋向于中位值的、中庸的做法。再加上超大上下文的性能问题和质量问题还无法解决,就算有Llama 4 Scout(Llama 4 Scout: 一种大型语言模型,以其支持的超大上下文窗口而闻名)这种能支持1000万token窗口的模型,大家在日常工作中,还是习惯用32k-128k的小窗口。而小窗口就意味着大模型会更依赖训练数据中的中庸做法,而不是针对当前项目代码做最优解。
所以你会看到很多人抱怨,AI经常无视代码里已有的工具库,为了在某个地方调用一次,而完整重造了一个新的;或者无视编程范式,在函数式代码旁边来一段面向对象编程;又或者无视项目状况,升级一些重要的依赖,从而带来没必要的兼容问题。随着市面上的代码越来越多地由AI生成,用来训练的代码数据也一样,AI自己拉出来,再自己吃回去,形成闭环,屎山只会越来越高。直到哪天它们搞出大新闻,企业可能才会想起向人类程序员求救。而这,或许就是重构的重生时刻。