抽象与复用:第三次重复才抽,一次抽错的代价

陆远把三份相同的导出代码抽成一个函数,省下三十五行;九个月后一次需求分叉,让它变成七个调用点、十一个参数的负担。本文用三次规则、抽错后 140 分钟对 70 分钟的账与两年回本曲线,算出抽象该在哪一刻发生、又在哪一刻该拆掉。

重复三次之前看到的是巧合,三次之后看到的才是形状。

一、他抽早了一次

陆远在第 7 个月做了第一次「正经抽象」。projects、files、analytics 三个模块各有一段导出 CSV 的代码,各 30 行,几乎一样;他花 1 小时抽成一个 40 行的公共函数,三处各留 5 行调用。总行数从 90 降到 55,他很满意,还在提交信息里写了「消除重复」。

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

第 9 个月,billing 需要一个带租户表头、金额按分转元的导出,公共函数加了两个参数;第 11 个月,files 需要流式写出大文件,又加了一个开关;到第 16 个月,这个函数有 11 个参数、被 7 处调用,每加一个参数都要把 7 个文件重新看一遍。

陆远后来复盘,抽取那天三个调用点确实同时存在,但他没问一句:这三处的输出格式会不会分叉。答案是会,而且第一次分叉只隔了两个月。

更麻烦的是,这个函数已经不在任何一次改动的上下文里。改 projects 时他只记得要传 tenant 与 format,改 billing 时只记得要传金额单位,第 7 个参数是给谁加的,要翻三次提交才想得起来。

抽象的收益在第二次复制时看不见,在分叉时才结算。 三处代码一样的时候抽出来最省事,也最容易抽错;真正的成本不在抽取那一天,而在第一次需求分叉的那一周。

三次规则是一道时间闸,不是一道数量闸。 它的作用不是数到三就抽,而是让你等到三个真实调用点同时存在,再判断它们会不会往不同方向走。

抽错比不抽贵两倍。 不抽时改 7 处各 10 分钟是 70 分钟;抽错后改 7 个调用点各 20 分钟是 140 分钟,因为每处都要先理解那个通用函数的 11 个参数、再决定自己该传哪一组。

行数变少不等于变好。 90 行变 55 行省了 35 行,但如果这 35 行换来的是 7 个调用点的耦合,这笔账要按未来两年的改动次数重算,而不是按抽取当天的 diff 重算。

二、三次规则与一次判断

三次规则管的是「什么时候可以抽」,不管「抽成什么样」。它把第一次与第二次都按「先别动」处理,只把第三次留作判断点。

第几次出现动作判断依据
第一次直接写,不抽只有一个调用点,抽出来只是多一层跳转
第二次复制,不改两份允许不同步,先看它们接下来往哪走
第三次同时存在才抽先问「未来 6 个月会不会分叉」,会分叉就不抽

规则的关键在第三行。三次同时出现只说明「现在一样」,不说明「以后一样」;分叉概率超过一半时,抽象只是把三处重复换成一处分支,重复还在,只是藏进了参数里。

判断分叉要问三个问题:它们的输入来源是否同一个、输出格式是否同一个、变更节奏是否由同一个人决定。三个都相同,抽;有一个不同,先复制。

抽的时机也要看改动频率。三处代码一年都不动一次,抽不抽都省不下时间;三处每月都改,且改法一致,抽取的收益才立刻兑现。

三次规则也不要求三次长得一模一样。第三次出现时如果已经有两处不同,那不是「可以抽了」,而是「已经分叉了」,这时候抽出来只会把两份差异变成两个布尔参数。

反过来,一个只有两个调用点、每次只省 3 行的函数,即使第三个调用点出现了,也不值得抽。抽象的门槛是「省下的改动次数乘以每次省下的分钟数」,不是「看起来像重复」。

三、一次真实抽取与一次抽错

把 90 行压成 55 行的那次抽取,是规则用对了的样子。三个调用点在同一周出现,输入都是同一张表的查询结果,输出都是同样的 CSV 列,变更节奏也由陆远一个人掌握。

状态行数调用点改一处的成本
抽取前903 处各 30 行改 3 个文件,各 10 分钟
抽取后551 个函数 + 3 处各 5 行改 1 个文件,10 分钟
净变化−353一致改动时省 20 分钟

那 40 行里没有一行是新的业务逻辑:参数校验 8 行、列名映射 12 行、写出 14 行、错误处理 6 行。抽取时只搬不改,搬完三个调用点各自只剩「查数据、调函数、返回」5 行。

抽取本身花了 1 小时。按每月一次一致改动算,每次从 30 分钟降到 10 分钟,省 20 分钟,三个月回本;这是抽对了的样子,之后又有 4 处调用主动接进来,才有了后来的 7 个调用点。

抽错的样子不同:7 个调用点、11 个参数,一次一致改动变成 7 × 20 = 140 分钟,不抽只要 7 × 10 = 70 分钟。多出来的 70 分钟不是抽象本身的开销,是「每处都要判断自己该传哪组参数」的开销。

还有一种更隐蔽的抽错:抽出来的函数只有 2 个调用点,每个调用点只省 3 行,但函数本身 400 行。净多 394 行,且这 400 行只有你一个人维护,每次需求变化都要在里面找开关。

四、复算:两年的账与 1.25 年回本

一次抽取:3 × 30 = 90 行
抽后:40 + 3 × 5 = 55 行
节省:90 − 55 = 35 行

抽错:7 × 20 = 140 分钟
不抽:7 × 10 = 70 分钟
倍数:140 ÷ 70 = 2.0

两年 12 次抽象:12 × 1 = 12 小时
拆掉 4 次:4 × 2 = 8 小时
总成本:12 + 8 = 20 小时
成功 8 处:8 × 2 = 16 小时/年

第一年:16 − 20 = −4 小时
第二年:+16 小时
累计回本:1 + 4 ÷ 16 = 1.25 年

过度设计:400 − 2 × 3 = 394 行

第一年的账是负的,这一点必须提前接受。12 次抽象里有 4 次会被拆掉,拆的时候还要再花 2 小时;20 小时的成本在第一年摊完,收益只有当年那 16 小时,净 −4 小时。

第二年开始转正。8 处成功的抽象每年各省 2 小时,累计到第二年末是 +12 小时;把第一年的 −4 抵掉,等于在第 15 个月回本。这个数字说明抽象是长期动作,不适合用季度报表考核。

拆掉的那 4 次不算失败。它们提供了「这两个调用点其实会分叉」的证据,没有那 4 次,你不会知道边界该画在哪;8 小时买的是判断力,不是代码。

394 行那个例子是反面的极端:抽象的成本主要在维护,不在书写。一个 400 行的配置层只有 2 个调用点,每个省 3 行,净多 394 行,还要在每次需求变化时同步修改。

复算里最容易被忽略的是「成功」的定义。8 处成功的标准不是「抽完没报错」,而是「连续 12 个月没有被拆掉、且调用点只增不改参数」;按这个标准回头看,那 4 次拆掉是迟早的事。

这组数字对分叉率敏感。如果 12 次里被拆掉的是 6 次而不是 4 次,成本变成 12 + 12 = 24 小时,成功只剩 6 处、每年省 12 小时,第一年净 −12 小时,回本从 1.25 年推到 2 年。分叉判断错两次,一年的收益就全被拆解成本吃掉。

敏感性的用法不是算得更准,而是决定「要不要抽」。当拆掉的比例可能超过一半时,抽取的期望收益是负的,这时候复制两份反而是更便宜的选择;这也是三次规则里那句「先问会不会分叉」真正的分量。

五、行动清单与复算验收

  • 在代码里搜出重复三次以上的片段,只登记位置与行数,不立刻抽。
  • 对每个候选写一行「输入、输出、变更节奏」,三项都相同才进入抽取。
  • 抽之前回答「未来 6 个月会不会分叉」,会分叉的比例超过一半就继续复制。
  • 抽取时把公共函数的参数控制在 3 个以内,超过就说明它其实在做两件事。
  • 抽完记录三个调用点各自的调用行数,总行数必须比抽取前少。
  • 每季度回看一次已抽的函数,参数超过 6 个的列进拆分候选。
  • 拆掉一个抽错的函数时,把两边的差异写成测试,避免下次再抽回去。
  • 新建的「万能配置层」超过 100 行且调用点少于 3 个,直接删掉。

验收标准:如果一次抽取不能让三个调用点的总行数从 90 降到 55,也不能让一致改动从改三处变成改一处,那么这次抽取就不该发生。

清单里最容易被跳过的是第三条。分叉判断写在抽之前,成本是一句话;写在抽之后,成本是 7 个文件的返工,以及 140 分钟的重复劳动。

最后一条针对的是另一种冲动:把「以后可能会用到」写成配置。配置层的每一行都要有人读,调用点少于 3 个时,它就是纯粹的成本。

使用边界与实操验收

三次规则不适用于安全与合规代码。 权限判断、金额计算、加密调用这类逻辑,第一次就该抽成唯一入口;它们的重复不是巧合,是风险,不能用「等第三次」来拖。

跨模块复用要额外加一道边界检查。 同一个函数被 projects 与 billing 同时调用,等于让两个模块通过它耦合;抽之前先确认这条依赖方向是否允许,别为了省 35 行造出一条逆向依赖。

复制两份是健康状态,不是欠债。 两份代码各自演化,往往是两个需求确实不同;强行合并会让下一次分叉更难拆,也会让你在改其中一处时误以为另一处已经同步。

抽象的收益只在改动发生时兑现。 一年不动一次的代码,抽得再漂亮也不省时间;判断要不要抽,先看它一年被改几次,而不是看它被复制了几次。

什么时候该停下来。 当一个公共函数的参数超过 6 个、或者它的名字里出现「and」,说明它已经在承担两条职责,该拆而不是继续加开关。

关键参考与核验范围

核验日期:2026-10-26。以下文档用于核对重构手法、重复代码的处理方式与技术债的定义,本文所有数字均取自全组教学基线,不取自这些文档。

  • Refactoring catalog :用于核对「提取函数」「内联函数」等手法的适用前提,以及它们互为反向操作的关系。
  • TechnicalDebt | Martin Fowler :用于核对债务与利息的隐喻,以及为什么「过早抽象」通常不计入债务。
  • The Twelve-Factor App :用于核对「显式声明依赖」与「配置外置」的边界,避免把配置层当成抽象。

引用口径。 文中 90、55、35、140、70、20、16 等数字全部来自全组教学基线;外部文档只用于核对手法名称与概念边界,不提供任何行数或工时基准。

核验范围。 上述三个入口在 2026-10-26 实测可公开访问(HTTP 200);未核验各页面的最近更新时间与示例代码所用语言版本。

如何自行复核。 取最近半年的一次抽取,数出抽取前后的总行数与调用点数量;再记录一次分叉改了几个文件、每个文件花了多少分钟。两项与本文的 35 行、140 分钟对不上,说明你的代码库还不适用这组基线。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元