
凌晨两点,你盯着 diff 里那行 if is_premium and not is_trial and billing_cycle == 'annual',光标悬在 "Request Changes" 上方迟迟没有按下。Pull Request 的标题是"合并 customer 和 broker 的折扣计算逻辑"。两段代码确实长得几乎一样——加载一条记录、更新一个百分比字段、写回数据库。一位工程师发现了这个"重复",提取出一个带 entity_type 参数的统一方法。看起来干净、合理、DRY。
但你知道 customer 的折扣明天就要按阶梯价计算,而 broker 的佣金逻辑未来两年都不会变。现在强行合并,表面上消除了重复,实际上把两个演化方向完全不同的概念焊在了一起。这就是 Sandi Metz 十年前警告过的陷阱。2026 年 6 月,她的文章以 409 分、272 条评论重新登上 HN 榜首——在一个 AI 可以一次性替你生成五百行"看起来正确"的代码的时代,这条原则比任何时候都更需要被重新讨论。
Sandi Metz 画了一张腐烂地图
Metz 在 2014 年 RailsConf 演讲中第一次说出"duplication is far cheaper than the wrong abstraction",2016 年写成博客文章。她的论证极其朴素,不依赖任何理论框架——她只是描述了一个所有人经历过却很少人命名过的退化过程:
程序员 A 发现了重复代码。他提取出一个公共方法或类,替换掉所有重复点,满意地走开。时间过去,新需求到来,现有抽象"几乎"够用。程序员 B 接手,出于对既有代码的尊重,他不另起炉灶,而是给方法加一个参数、再在方法内部加一个条件分支。然后是第三个需求、第四个参数、第五个 if-else。到第八步时,你出现了,对着数千行纠缠在一起的条件逻辑,试图理解哪些分支属于哪个调用者。
Metz 给出的解法同样简洁:把抽象内联回去,让每个调用者只保留自己真正需要的代码,然后从头观察——哪些相似性是真实的,哪些只是"看起来像"。
这段话之所以有穿透力,是因为它打破了程序员群体中一条近乎宗教的信念:重复就是邪恶,消除重复天然正确。
DRY 的历史包袱:从数据库到代码库的误译
DRY 原则由 Andy Hunt 和 Dave Thomas 在 1999 年《程序员修炼之道》中提出。原文的表述是:"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." 重点落在"知识"上,不是落在"字符"上。一段 SQL 查询、一种业务规则、一个配置值——这些是知识。两段恰好相似的 for 循环,可能根本不是。
但产业在传播过程中逐渐压缩了这个区分。"不要重复自己"变成了"不要出现重复的代码行"。一个启发式原则被拔高为硬性规则后,催生了大量本来不该存在的抽象:泛型 Repository 基类、万能 Processor 方法、参数表比业务逻辑还长的 service 函数。
Metz 做的事本质上是对 DRY 的校准:它反的是「过早的 DRY」。这句话也在 HN 讨论中被反复强调——"这篇文章不是说不要抽象,是说不要强求抽象"。
识别"错误抽象"的工程信号
在 HN 评论区,多位工程师分享了自己识别错误抽象的准则。"这段代码做的是同一件事,还是只是看起来相同?"——这是被多次引用的核心判据。以下信号若叠加出现,抽象大概率是错的:
参数驱动的条件分支。 一个方法接收布尔或枚举参数,内部用 if-else 分发到几乎不重叠的代码路径。每增加一个新参数,调用者需要理解的状态空间就乘上一层。
修改一个调用者的行为,却不得不同时为其他调用者"兜底"。 这意味着调用者之间不存在真正的同变关系。它们只是恰好今天跑着相似的代码。
抽象没有自明的存在理由。 一个健康的抽象,不看调用者就能理解它的职责。如果每次读它都要追溯三个调用方的上下文才能明白这段逻辑在干嘛,这个抽象已经失去了它最大的价值——降低认知负荷。
添加新功能时,你首先想到的是绕过这个抽象,而不是复用它。 这是最可靠的信号。人的直觉往往比理性化的事后解释更早捕捉到结构性问题。
HN 的核心辩论:单一真相 vs. 局部性
这次 HN 讨论中,两条高赞评论精准地划定了争议的边界。
用户 lg5689 的主张代表了"抽象优先"一派的核心理由:"应该始终遵循单一真相来源原则。如果两处重复代码在发生分歧时会构成 bug,就应该重构。重复会在代码中制造隐形的长距离耦合。"这条逻辑来自一个干净的工程直觉:当同一段业务规则分布在两个位置,未来某天改了一个忘了另一个,bug 就埋下了。
用户 jonahx 的回应则指向 Metz 真正关切的场景:"从根本上说,文章讨论的正是你还不知道真相来源有几个的情况。这两个位置用的是同一套算法,还是略有不同的版本?更重要的是,它们会因同类的理由而变更吗?最关键的是,错误的抽象破坏了局部性——而那其实是你修改代码时唯一真正在乎的属性。我只想做这一个改动,不用担心对系统不相关部分的副作用。"
两条评论都有道理,但适用场景不同。如果你确定两个位置代表同一个不变的事实——同一个税率、同一个加密算法、同一种数据校验规则——那么抽象是正确的选择,单一真相来源的收益远超抽象本身的开销。
问题在于,在这个行业里,我们高估了"看清两段代码是否同义"的能力。Metz 举例的两个计算看起来很相似:加载 customer 记录、更新折扣百分比;加载 broker 记录、更新佣金百分比。它们今天碰巧都是一种"加载实体-更新百分比"的模式。但 customer 折扣的业务逻辑随时可能改成梯度计算,而 broker 佣金继续维持单一百分比——因为这两个字段在法律、合同、会计意义上的性质完全不同。
"看起来相同的代码"和"代表同一真相的代码"之间的那条线,比大多数工程师愿意承认的更难画准。
AI 代码生成如何放大这个问题
这恰恰是 AI 编程工具大面积使用后,Metz 的文章重新被推上榜首的原因。
LLM 在代码生成上有两个结构性倾向。第一,它天然倾向于"消除看起来的重复"。当你用一个 prompt 生成两个类似功能模块时,模型会从训练数据中提取最"标准"的合并方式,给出一个带参数表示的抽象。它不问你 customer 和 broker 的业务边界是什么——它没有参与过需求讨论。它只是在统计意义上找到了最优的共享表示。
第二,更隐蔽也更危险的是,LLM 生成的抽象异常光滑。命名合理、缩进规范、参数排列有逻辑感。一个人类工程师写的滥抽象往往有异味——命名别扭、结构松散、你能感觉到它在勉强适应。LLM 的错抽象看起来专业、自信、无懈可击。审查者更容易放行。
HN 讨论中多位评论者指出了这个张力。有人说"LLM 是天然的反抽象机器",因为它们不理解业务语义,只理解表面模式。也有人说"LLM 让复制的成本大幅降低,所以抽象需要高得多的论证门槛"。还有一条更尖锐的观察:"我花最多时间思考的是,怎么跟 LLM 解释一个存量代码库实际是如何工作的,而又不让它因为误解而扭曲它。"
一个有趣的工程现象是:AI 生成的大量代码倾向于复制而非抽象。原因不是模型懂 Metz 的原则——它在逐次请求之间缺乏跨文件的持久记忆。它不知道上一个 session 里写过类似的东西——除非你把相关代码塞进上下文窗口。于是 AI 产物中出现了大量"看起来应该被抽象但没被抽象"的代码块,以及"已经被抽象但抽象方向完全错误"的代码块。两种错误集中在同一个仓库里,这或许是 AI 辅助编程带给维护者的新日常。
在两种错误之间做选择
Metz 的立场经常被简化为"复制比抽象好",这不够公平。她真正的意思是:如果你必须在复制和错误抽象之间选一个,选复制。 这是个二阶原则——它不告诉你什么是对的,它告诉你当你不确定什么是对的时候,哪个方向上的错误成本更低。
HN 高赞评论提供了一条实用的操作准则——"三次规则":第一次出现,写下来。第二次出现,容忍重复,但开始观察。第三次出现,考虑抽象——而且只沿着真正在变化的那个轴做抽象。这条准则隐含了一个关键前提:你需要时间让真正的模式浮现出来。代码在仓库里跑一段时间后,哪些调用位置会同频变化、哪些会分道扬镳,才会变得可辨识。
另一位评论者的总结更尖锐:"DRY 的反面不是重复,是 WET——Write Everything Twice。写两遍,然后观察。写三遍再动手。"
数据之后的工程判断
HN 的投票数据——409 分、272 条评论——说明这件事触及了工程师群体中一条尚未愈合的裂缝。大家知道 DRY 可能用错。问题在于一代代新工程师在入职时接收到的教育仍然把"消除重复"当作代码审查中不可挑战的优先级。
在一个 AI 可以替你写出合规代码的时代,真正的稀缺能力不再是"怎样抽象",而是"何时抽象"。后者需要的不是技巧,是耐心、是对业务领域持续观察后形成的判断,以及在 sunk cost 面前敢于把抽象拆回去的冷静。Metz 的原话回响到今天:"当面对错误抽象时,最快的前进方式就是后退。"
在这件事上没有终极答案。笔者对这两派争议不持有绝对立场。抽象是软件工程中少数真正意义上的基石概念,但它的价值高度依赖时机和上下文。本文不倡导用复制替代抽象。想指出的是一条更窄的判断:在代码由人和机器交替产出的新常态下,"等一等再抽象"的代价也许比我们长期以来认为的更低,而"抽错了之后再拆"的代价可能比我们预想的更高。
笔者声明:本文基于对 Sandi Metz 原文、2026年6月 HN 讨论及相关工程文献的梳理与分析,不构成绝对的技术建议。工程决策需要结合具体上下文——团队规模、业务阶段、代码库年限、测试覆盖率——这些变量中的任何一个都可能使本文的判断方向翻转。
夜雨聆风