你一定听过"DRY"——Don't Repeat Yourself。然后你做了一件每个开发者都做过的事:看到两段相似的代码,二话不说提取成公共函数。
三个月后,你站在那个函数面前,盯着 8 个参数和 14 行 if/else,心里只有一个念头:我当初为什么要写这个东西?
这篇文章聊的就是这件事——过早抽象为什么比重复更贵,以及"该出手时再出手"到底怎么判断。
核心结论先放这:DRY 反对的是知识分裂,不是字面重复。在你确认两段代码背后是"同一份知识"之前,重复是更安全的选择。
一、DRY 被误读了
Kent C. Dodds 提出 AHA(Avoid Hasty Abstractions) ——避免草率的抽象——不是要推翻 DRY,而是纠正一个流行的误读。
DRY 的原始定义来自《The Pragmatic Programmer》:
系统中每一块知识都必须有唯一、明确、权威的表达。
关键词是"知识",不是"代码长得像"。
两段代码字面相似,不等于它们表达的是同一份知识。一个是用户登录的请求逻辑,一个是订单提交的请求逻辑——它们的 fetch 调用写法一模一样,但它们各自的演化方向完全不同。把它们合并进同一个 useRequest 是在合并两个不同物种的基因。
二、错误抽象的演化路径
Sandi Metz 用一段近乎临床的精准描述了这个过程:
| 阶段 | 发生的事 |
|---|---|
| 1 | 你看到重复,提取抽象 |
| 2 | 新需求来了,抽象"差不多"适用 |
| 3 | 你加一个参数、一条分支让它"刚好"适配 |
| 4 | 重复上述步骤 N 次 |
| 5 | 抽象变成一座参数迷宫,没人敢碰 |
错误的抽象不是一瞬间产生的,它是每次"改一点点应该没关系"的累积。
这背后有三重心理陷阱:
• DRY 教条——"重复就是罪"的条件反射
• 锚定效应——已有代码对你的思维有引力,你会不自觉往里塞
• 沉没成本——"都投入这么多了,不能白费"
经济学对最后一条有个简洁的回答:止损。你在一只股票上亏了 30%,继续持有不会让亏损消失,只会让你错过更好的机会。代码完全相同——Sandi Metz 说得好:"最快的前进方式是后退。"
三、AHA 的判断框架
AHA 不是 DRY 的反面。它是一个时机判断器:
| 信号 | 该做什么 |
|---|---|
| 两段代码"长得像",但你不确定它们的演化方向 | 保持重复,打个 // DUPE:xxx 标记 |
| 第三次、第四次看到同样的模式,共性向你"呐喊" | 这是提炼的信号 |
| 你能清晰说出"哪些部分是变的,哪些是不变的" | 可以安心抽象 |
| 新需求让现有抽象需要加第 N 个参数 | 停下来想:该内联回去了? |
Kent 给了一条极简原则:
Optimize for change first. 首先为变化做优化。
不要问"这段代码重复了几次",要问"这段代码未来会往哪些方向变"。如果你不确定——先别动它。
四、进化论视角:先隔离,后分化
生物学里,物种分化的前提是地理隔离:同一种群被山脉或海洋分开,在不同环境中各自适应,差异积累到一定程度后自然分化成两个物种。
代码的演化完全相同。
两段"看起来一样"的代码,只有在各自的业务场景中运行足够久——用户登录加了二步验证,订单提交加了库存预检——差异自然浮现后,你才真正知道它们是"同一物种"还是"两个物种"。
过早合并,就像强行把狼和狗关进同一个基因库——系统会用层层 if/else 向你抗议。
这就是为什么 AHA 说"先接受重复":重复是让代码在各自的生态位中充分分化的隔离期。等到分化结果明朗,你再决定合并还是永久分离。
五、前端的真实代价
这对前端尤其重要。回想一下你见过的这些:
组件层:一个 <Card> 组件,为了适配"商品卡片""用户卡片""文章卡片"三种场景,接受 12 个 props、内部 6 条 if (variant === ...)。改任何一种卡片都得通读其余两种的逻辑。
Hook 层:一个 useRequest,为了同时支持 GET/POST/分页/轮询/取消/缓存,变成了一个拥有 30 行类型定义的"小框架"。团队新人需要读 3 天才能上手。
Service 层:一个 apiClient,把所有接口的请求/响应/错误处理统一起来——直到某个接口需要特殊的重试策略,你开始在"统一层"里打补丁。
这些都不是"重复太多"造成的问题,而是"抽象太早"造成的问题。
对比一下:三个独立的卡片组件,各 40 行,互不耦合。改"商品卡片"不需要知道"用户卡片"的存在。某天你发现三者确实共享一个"卡片容器 + 圆角阴影"的外壳——好的,这时候只提取 <CardShell> 负责外壳,内容各自处理。
少量重复是隔离的代价,错误抽象是耦合的代价。前者线性增长,后者指数爆炸。
六、给 AI 时代的一句话
现在 AI 能帮你快速生成代码,"写重复代码"的体力成本趋近于零。但"理解一个错误抽象并把它拆回来"的认知成本没有降低——甚至更高,因为 AI 生成的抽象可能连原作者都没有。
所以 AHA 在 AI 时代反而更重要了:生成代码的成本为零,但生成错误抽象的伤害不变。
七、一张串联表
本周如果你同时在看 SoC / SRP / DI / DIP / AHA 这些架构原则,这张表帮你理清它们各自回答的问题:
| 原则 | 回答什么问题 |
|---|---|
| SoC(关注点分离) | 边界画在哪 — 哪些逻辑应该住在一起 |
| SRP(单一职责) | 一个模块只为一个角色负责 — 谁能要求它改变 |
| DI / DIP(依赖倒置) | 依赖方向指向哪 — 高层不该依赖低层细节 |
| AHA(避免草率抽象) | 何时出手 — 抽象的时机比抽象的技巧更重要 |
它们不矛盾,而是一套完整的决策链:先用 SoC/SRP 决定边界,再用 DI/DIP 决定依赖方向,最后用 AHA 决定抽象何时出手。
如果你只想带走一句话,我建议记这个:
抽象是把药——对症下药能治病,没病乱吃会产生耐药性。先确诊,再开方。
参考原文:
• Kent C. Dodds — AHA Programming
• Sandi Metz — The Wrong Abstraction