别急着抽象,先让代码重复一会儿

0 阅读6分钟

你一定听过"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

qrcode_for_gh_6a9e7f3719d6_344.jpg