2020年初,React核心团队成员Dan Abramov在深夜完成了一次"完美"的重构。他看到同事实现的图形编辑器代码中存在大量重复——每种形状(矩形、椭圆、文本框)的缩放逻辑都包含相似的数学运算。作为一个追求简洁的程序员,他花了几个小时将这些重复代码抽象成一个优雅的组合系统,代码量减少了一半,所有重复都消失了。

第二天早上,他的老板请他进了一对一会议室,礼貌地要求他回滚这次修改。

这不是一个关于"代码不够干净"的故事。恰恰相反,那次重构的代码极其干净——只是它完全错了。那个"优雅"的抽象锁死了未来的需求变化路径。当产品需要为不同形状的不同拖拽点添加特殊行为时,Dan的抽象变成了一个需要层层条件判断的复杂怪物,而原始的"重复"版本却可以轻松扩展。

这个案例揭示了一个被广泛忽视的困境:代码复用——这个被奉为圭臬的原则——正在以隐蔽的方式吞噬软件项目的可维护性

DRY的原始面目:知识重复,不是代码重复

1999年,Andy Hunt和Dave Thomas在《程序员修炼之道》中首次明确提出DRY原则:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

每一条知识必须在系统中有一个单一、明确、权威的表示。

注意这个定义的关键词:knowledge(知识),而不是code(代码)。这正是二十多年来被广泛误读的地方。

Dave Thomas在2019年的书籍20周年纪念版中专门澄清了这一点:

Many people took it to refer to code only: they thought that DRY means “don’t copy-and-paste lines of source.” […] DRY is about the duplication of knowledge, of intent. It’s about expressing the same thing in two different places, possibly in two totally different ways.

一个电商系统中的"发货逻辑"是一块知识——它应该只存在于一个地方。但两个恰好看起来相似的代码片段,可能代表完全不同的业务意图。把它们强行抽象在一起,就是将两块本不相关的知识耦合了起来。

Martin Fowler在总结Kent Beck的简单设计四原则时,将"无重复"列为第三条,但他特别强调:消除重复的过程本身是一种设计活动,它驱动出更好的设计。关键在于这个过程是响应式的——你发现重复,理解其本质,再决定是否抽象——而不是预判式的——看到相似代码就急于抽象。

过早抽象的陷阱:Sandi Metz的洞察

Ruby社区知名专家Sandi Metz在2016年发表了一篇引起广泛共鸣的文章《The Wrong Abstraction》。她描述了一个典型的演进模式:

  1. 程序员A发现重复代码
  2. 程序员A提取重复,创建新抽象
  3. 时间流逝
  4. 新需求出现,当前抽象"几乎"完美
  5. 程序员B添加参数和条件分支来处理特殊情况
  6. 更多需求到达,更多参数,更多条件
  7. 代码变得难以理解

这个模式的可怕之处在于:现有代码会产生一种强大的引力。它的存在本身就暗示着它是"正确且必要的"。我们投入了时间创造它,沉没成本谬误让我们不愿放手。

Metz给出的建议反直觉但深刻:

Duplication is far cheaper than the wrong abstraction.

重复远比错误的抽象便宜。

当你发现自己在往"共享"代码里添加参数和条件分支时,抽象已经错了。Metz的解法更激进:回退。将抽象代码内联回每个调用点,删除那些条件分支,让每个调用点只保留它真正需要的代码。只有当重复的模式重新清晰展现时,才考虑重新抽象。

这听起来像是"浪费时间",但实际经验表明:当抽象已经腐化时,继续在错误基础上修补,只会让代码越来越难以理解和修改。最快的前进方式,有时是后退。

Rule of Three:抽象的时机边界

那么,什么时候应该抽象?Kent Beck在极限编程实践中提出了"Rule of Three"原则,被Jeff Atwood在Coding Horror博客中广为传播:

  • 第一次,直接实现
  • 第二次,注意到了重复,但忍住
  • 第三次,才进行抽象

这个原则背后有一个更深层的观察:构建真正可复用的组件,比构建单用途组件难三倍

这不是比喻。Robert Glass在《Facts and Fallacies of Software Engineering》中引用的研究表明:

It is three times as difficult to build reusable components as single use components, and a reusable component should be tried out in three different applications before it will be sufficiently general to accept into a reuse library.

Jeff Atwood深信这个原则,以至于他创建的两个公司(Stack Overflow/Stack Exchange和Discourse)都建立在这个理念之上。Discourse在发布时甚至明确告诉用户"现在还不能购买",因为他们需要先找到三个合作伙伴来验证软件是否足够通用。

抽象泄漏定律:所有的抽象都在撒谎

2002年11月,Joel Spolsky发表了一篇影响深远的文章《The Law of Leaky Abstractions》。他提出了一个令人沮丧但无可辩驳的定律:

All non-trivial abstractions, to some degree, are leaky.

所有非平凡的抽象,在某种程度上,都会泄漏。

Spolsky用TCP协议作为经典案例。TCP承诺提供一个可靠的数据传输通道——你发送数据,它保证到达,顺序正确,内容完整。但TCP是建立在IP协议之上的,而IP是不可靠的:数据包可能丢失、乱序、重复。

TCP通过重传、排序、校验等机制,在不可靠的IP之上实现了可靠性。这是一个伟大的抽象。但它仍然会泄漏:如果网线被咬断,TCP无能为力;如果网络拥塞,TCP的传输会变慢,但抽象告诉你"这是可靠的连接",它隐藏了延迟的变化。

这个定律对软件开发有深远影响。每一个抽象——无论设计得多么精妙——都无法完全隐藏底层的复杂性。当你使用一个ORM来抽象数据库操作时,你迟早会遇到N+1查询问题;当你使用一个远程文件系统抽象时,你迟早需要处理网络超时。

Spolsky指出一个悖论:抽象本应简化我们的生活,但抽象泄漏意味着我们仍然需要理解底层细节。你教会一个新手使用STL字符串类,但有一天他们写下"foo" + "bar"然后得到奇怪的结果,你又要回头教他们关于C风格字符串和指针的一切。

这意味着,随着抽象层越来越厚,成为一个熟练程序员反而越来越难。不是因为你需要知道的东西变少了,而是因为你需要知道的东西更多了——每一层抽象的细节。

Hyrum定律:当抽象变成枷锁

Google工程师Hyrum Wright提出了另一个影响深远的观察,被称为Hyrum定律:

With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.

当API的用户足够多时,无论你在契约中承诺什么:你系统的所有可观察行为都会被某人依赖。

这与抽象泄漏直接相关。当一个抽象被广泛使用时,用户不仅依赖你文档中承诺的行为,还会依赖各种"恰好能工作"的边缘行为:某个函数恰好返回结果的顺序、某个操作恰好需要的毫秒数、某个错误消息恰好包含的文本。

这些隐性依赖形成了"隐性接口"。当你想要修改实现时,你必须同时维护显式接口和隐性接口。这就是为什么改变一个"内部实现"会意外破坏远处的代码——你以为你只暴露了接口,但用户实际上依赖了实现。

一个足够流行的抽象,其接口会"蒸发"——实现本身变成了接口。任何对实现的改变,都变成了破坏性变更。

数据说话:代码克隆的真面目

学术界对代码克隆(code clone)的研究,提供了更多实证支持。

NSF资助的实证研究表明,软件系统中通常存在7%到23%的重复代码。另一项对GitHub项目的分析显示,9%到31%的项目包含大量可在其他地方找到的文件。

但关键问题是:这些重复代码真的有害吗?

Concordia大学的研究团队分析了代码克隆的不一致修改。他们发现,只有1%到3%的不一致修改会引入缺陷。换句话说,97%以上的情况下,开发者能够有效地管理重复代码的演进。

Kapser和Godfrey的研究更进一步:他们识别出了11种代码克隆的使用模式,发现克隆往往是有目的的实现策略,而非懒惰的结果。在某些场景下,复制代码比创建复杂的抽象更合理。

这些研究结论与"重复代码是万恶之源"的教条形成鲜明对比。重复代码确实需要关注,但它不是自动需要消除的"缺陷"——它可能是一个信号,提示我们需要仔细思考是否值得抽象。

AHA原则:在DRY与WET之间

Kent C. Dodds提出了一个务实的折中方案:AHA(Avoid Hasty Abstractions,避免仓促抽象)。

这个原则承认DRY和WET(Write Everything Twice,写两遍)都有道理,但都过于教条。AHA的核心是:不要对何时抽象设定硬性规则,而是在时机感觉对的时候抽象,不要害怕在到达那个点之前复制代码。

Dodds将Sandi Metz的洞察总结为:

Prefer duplication over the wrong abstraction.

宁可重复,也不要错误的抽象。

他还添加了一个重要原则:优先优化变化。我们不知道代码的未来会怎样。可能花了几周优化性能、设计完美的抽象API,第二天发现需求完全变了。唯一可以确定的是——事情会变化。如果代码永远不需要修改,它长什么样其实无关紧要。

这听起来像是无政府主义,但Dodds强调这需要正念:不是允许任何混乱,而是意识到我们无法预知未来需求。复制代码直到你对使用场景有足够信心,让重复模式自然显现,再进行抽象——这比预判性抽象安全得多。

何时复制,何时抽象:一个实用框架

综合以上研究与实践经验,可以总结出以下判断框架:

应该复制的情况:

  • 两个代码片段代表不同的业务知识,只是恰好看起来相似
  • 抽象会引入不必要的耦合,将本应独立演进的部分绑在一起
  • 重复只出现一两次,且没有明确的共性模式
  • 抽象后需要添加参数或条件分支来处理差异

应该抽象的情况:

  • 同一块业务知识出现在三个或更多地方
  • 修改一处必须同时修改其他地方(真正的知识重复)
  • 抽象后代码更清晰,没有引入复杂的条件逻辑
  • 抽象的边界明确,调用者不需要了解内部细节

判断的黄金法则: 当你在为"共享"代码添加第四个参数、第五个条件分支时,停下来。抽象已经错了。最快的修复方式是后退——内联代码,删除抽象,让重复重新展现其真实面目。

四十年困境的本质

代码复用的困境,本质上是简化与灵活性之间的永恒张力

抽象的目标是简化:隐藏细节,暴露接口,让使用者不必关心"如何实现"。但简化是有代价的:你放弃了控制,放弃了选择权。当需求变化时,你能否在抽象的约束下找到出路?

1999年提出DRY原则时,Hunt和Thomas的目标是消除"知识的重复"。但二十多年来,这条原则被简化成了一个口号:“不要复制代码”。这个简化版本丢失了关键的区分——知识重复 vs. 代码重复——而这正是问题的根源。

真正的工程智慧不在于"绝不重复"或"绝不抽象",而在于判断何时该复制、何时该抽象。这种判断无法简化为规则,它需要经验、需要思考、需要理解业务。

Dan Abramov在那次失败的抽象后写道:让整洁代码指引你,然后放手。代码整洁本身不是目标,它只是帮助我们在复杂的系统中找到方向。真正重要的是代码如何与团队一起演进——不是它看起来怎样,而是它如何变化。

重复不是敌人,错误的抽象才是。知道何时应该后退,可能比知道如何前进更重要。


参考文献

  1. Hunt, A., & Thomas, D. (2019). The Pragmatic Programmer, 20th Anniversary Edition. Addison-Wesley.
  2. Spolsky, J. (2002). The Law of Leaky Abstractions. https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/
  3. Metz, S. (2016). The Wrong Abstraction. https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
  4. Abramov, D. (2020). Goodbye, Clean Code. https://overreacted.io/goodbye-clean-code/
  5. Atwood, J. (2013). The Rule of Three. https://blog.codinghorror.com/rule-of-three/
  6. Wright, H. Hyrum’s Law. https://www.hyrumslaw.com/
  7. Fowler, M. (2015). Beck Design Rules. https://martinfowler.com/bliki/BeckDesignRules.html
  8. Dodds, K. C. (2020). AHA Programming. https://kentcdodds.com/blog/aha-programming
  9. Kapser, C., & Godfrey, M. W. (2008). “Cloning considered harmful” considered harmful: Patterns of cloning in software. Empirical Software Engineering, 13(6), 645-692.
  10. Bettenburg, N., et al. (2009). An empirical study on inconsistent changes to code clones at release time. Working Conference on Reverse Engineering.
  11. Rahman, M. M., et al. (2019). An empirical study of code clones. NSF Public Access Repository.
  12. Glass, R. L. (2002). Facts and Fallacies of Software Engineering. Addison-Wesley.