一段共享代码最会骗人的地方,是它刚被抽出来时确实很漂亮。重复少了,名字有了,调用方干净了。直到新需求来了,差一点点就能套进旧抽象,于是有人加一个参数。再来一个需求,再塞一个 if/else。

Sandi Metz 在 2016 年《The Wrong Abstraction》里把这件事说得很硬:duplication is far cheaper than the wrong abstraction。她后面还有一句更像工程戒律:prefer duplication over the wrong abstraction。这不是反对抽象,而是提醒团队:抽象一旦错了,代价会比重复更阴。

坏抽象的故障链:从省事到没人敢动

这套故障链很短,也很常见。

阶段看起来在做什么实际发生了什么
看到重复抽出方法、类或模块代码短期变干净
新需求几乎适配加参数兼容抽象开始变形
更多需求进入继续塞条件分支共享代码变成分岔迷宫
团队接手维护不敢删、不敢拆每次改动都要猜旧逻辑

重复代码至少诚实。它把差异摊在你面前,让你看见边界在哪里。

坏抽象更麻烦。它把几个并不相同的需求压进同一个壳里,再用参数、默认值、条件分支把裂口缝上。表面统一,内部已经分家。

这里的关键不是“重复好不好”。长期重复当然会带来修改成本,也可能造成行为不一致。问题在于,错误抽象会把差异藏起来,让团队误以为自己拥有一个稳定设计。

很多技术债不是因为代码难看,而是因为代码看起来太像资产。

沉没成本才是最硬的锁

Metz 点破的心理机制很现实:代码越复杂,团队越觉得它不能删。

复杂看起来像投入。投入看起来像价值。于是大家会下意识保护它。哪怕它已经从抽象退化成条件堆。

这就是沉没成本在代码库里的样子。

“知止不殆。”这句话放在抽象上很准。会抽象不稀奇,知道什么时候停,什么时候撤,才是工程判断。

我不太买账那种“先沿着旧结构补一下”的稳定叙事。它短期省事,长期收税。每多一个为了保住旧抽象而加的参数,下一个维护者就多付一点认知成本。

对有维护经验的开发者,这意味着一个很具体的动作:别把“再加一个参数”当默认选项。先看这个参数是不是只服务某个调用方。如果是,把逻辑拉回调用方,比继续污染共享代码更安全。

对 Tech Lead,这件事更像评审规则,而不是个人洁癖。代码评审里最该盯的不是“有没有重复”,而是这些信号:

观察信号说明什么建议动作
参数持续增加抽象在吞新场景暂停扩展接口,检查调用方差异
分支按调用方分岔共享代码只剩壳考虑 inline 回调用方
改一处要验证很多无关场景抽象边界失真拆开风险路径,再谈复用
新人不敢动这段代码复杂度已经组织化给重构留排期,不要只靠顺手修

接下来最该观察的变量也在这里:共享代码是不是越来越依赖参数和条件分支来维持体面;一次小改动是不是总会牵动一串无关场景。

如果答案是,那段代码已经不是复用成果,而是风险中枢。

真正高级的是敢撤回错误设计

Metz 给的解法并不玄。不是喊一句“重构”,而是把抽象拆回现场。

可以按这个顺序做:

  1. 把共享抽象 inline 回每个调用方。
  2. 根据每个调用方真实传入的参数只保留它会执行的路径。
  3. 删掉不需要的分支。
  4. 让重复重新暴露真实边界。
  5. 从当前需求出发再判断有没有新的抽象。

这一步看起来像后退,其实是止损。

重复重新出现时,团队会更容易看见哪些地方真的相同,哪些地方只是当年硬凑在一起。抽象应该从相同处长出来,不该靠条件分支强行维持。

但限制也要说清楚。Metz 不是在鼓励复制粘贴,更不是说所有重复都该留下。短期保留几个条件分支,有时可以帮助观察模式。前提是短期。

如果一个共享函数开始靠参数决定人格,一个模块开始为不同调用方走不同暗道,就该停了。它也许曾经正确,但那一天已经过去。

软件设计里最难的不是写出抽象,而是承认抽象有保质期。很多团队缺的不是架构能力,是撤销能力。

能抽出来,是技术。能拆回去,是判断。