← 返回信息流
AI 资讯Hacker News·2 小时前

代码重复远比错误的抽象更廉价

原标题:Code duplication is far cheaper than the wrong abstraction

速览

本文探讨了软件工程中重复代码与抽象设计之间的权衡。作者认为,虽然代码重复通常被视为不良实践,但构建错误的抽象往往会导致更严重的维护负担和技术债务。因此,在缺乏明确需求或过度设计时,保留重复代码可能是更经济、更务实的选择。

AI 深度解读

代码重复远比错误的抽象廉价

背景

在软件工程领域,“抽象”通常被视为一种美德,旨在通过提取共性来减少代码重复(DRY原则)。然而,这种对抽象的盲目追求往往导致反效果。

这篇文章最初由 Sandi Metz 撰写并发布于其 Newsletter(Chainline Newsletter),随后因其引发的强烈反响而在其博客上重新发布。早在 2014 年的 RailsConf 大会上,Sandi Metz 在一次题为“所有细微之处”(all the little things)的演讲中提出了一个极具争议的观点:“重复远比错误的抽象廉价”(duplication is far cheaper than the wrong abstraction)。

当时,这一观点在开发者社区中引发了两极分化的反应。虽然有人质疑其合理性,但更多的人表达了强烈的共鸣,认为这戳中了软件开发中普遍存在的痛点。这种强烈的反应让作者意识到,“错误的抽象”是一个广泛且难以根除的问题。文章通过剖析这一现象的心理机制和技术后果,为开发者提供了一套应对策略。

核心内容

错误的抽象是如何形成的?

文章描述了一个典型的代码演化陷阱,通常涉及两位开发者(Programmer A 和 Programmer B):

  1. 初始抽象:程序员 A 发现了代码中的重复部分,将其提取并命名,创建了一个新的抽象(如方法或类)。此时,代码看起来完美无缺,程序员 A 满意地离开。
  2. 需求变更:随着时间的推移,出现了一个新需求,现有的抽象几乎能满足它,但并不完全适用。
  3. 妥协与污染:程序员 B 被指派实现该需求。出于对既有代码的尊重(或“荣誉感”),B 没有选择重构,而是试图保留原有抽象。为了适配新需求,B 向抽象方法添加参数,并加入条件逻辑(if/else)来根据参数值执行不同的操作。
  4. 恶性循环:原本通用的抽象现在针对不同情况表现出不同行为。当另一个新需求到来时,程序员 X 再次添加参数和条件判断。
  5. 代码腐烂:循环往复,直到代码变得难以理解。

沉没成本谬误的心理陷阱

为什么开发者明知代码变得复杂,却不愿重构?文章指出,这是“沉没成本谬误”(sunk cost fallacy)在起作用。

  • 代码即努力:现有代码的存在本身就在暗示它是正确且必要的。我们知道代码代表了投入的时间和精力,因此我们极度渴望保护这种投入的价值。
  • 误解复杂度:潜意识里,我们倾向于认为:“这段代码如此混乱,肯定花了很多时间才调试正确。它一定非常重要。让它付诸东流简直是罪过。”
  • 后果:这种心理压力迫使后来者继续修改现有代码,导致代码变成充满条件判断的过程,交织着各种模糊相关的概念。结果就是代码既难理解又易出错。

解决方案:逆向工程

当发现自己陷入上述困境时,作者建议抵抗沉没成本的诱惑,采取“最快的前进方式是后退”这一策略。具体步骤如下:

  1. 重新引入重复:将抽象代码内联(inline)回每一个调用者中。
  2. 局部适配:在每个调用者内部,利用传入的参数决定该特定调用者实际执行的内联代码子集。
  3. 清理冗余:删除该特定调用者不需要的代码片段。

通过这种方式,抽象和条件判断被移除,每个调用者只保留其所需的代码。重新审视这些决策后,往往会发现:虽然每个调用者表面上调用了共享抽象,但它们实际执行的代码其实相当独特。

一旦彻底移除旧的抽象,就可以从头开始,重新隔离重复部分并提取新的、更准确的抽象。

实际案例与心态转变

作者观察到,许多团队在试图用错误的抽象推进项目时举步维艰:添加新功能极其困难,每一次“成功”都让代码更复杂,使得下一步更难。

当团队将心态从“我必须保护对这段代码的投资”转变为“这段代码在一段时间内是有意义的,但我们可能已经从中汲取了所有能学到的东西”时,情况豁然开朗。一旦允许自己根据当前需求重新思考抽象,内联代码后,前进的道路变得清晰,新功能添加的速度和难度都得到了显著改善。

道德启示

  • 不要被困住:如果你发现自己正在向共享代码传递参数并添加条件路径,说明抽象是错误的。
  • 接受过时性:抽象可能在开始时是正确的,但那个时代已经过去了。
  • 尽早放弃:虽然偶尔积累一些条件判断有助于理解现状,但尽早放弃错误的抽象会比拖延带来更少的痛苦。
  • 重新引入重复是前进:当抽象错误时,最快的前进方式是后退。这不是撤退,而是向更好的方向前进。

关键要点

  • 核心原则:重复(Duplication)比错误的抽象(Wrong Abstraction)成本更低。错误的抽象会导致代码难以维护、扩展和修改。
  • 识别信号:如果在共享代码中频繁看到参数传递和条件分支(if/else),这通常是抽象错误的强烈信号。
  • 心理障碍:开发者往往受“沉没成本谬误”影响,因不舍得废弃已投入大量精力的代码而拒绝重构,导致代码库日益腐化。
  • 重构策略
    • 内联(Inline):将抽象代码复制回所有调用处。
    • 裁剪:在每个调用处删除不需要的逻辑,只保留特定需求所需的代码。
    • 重构:在消除混乱后,基于当前需求重新提取更准确的抽象。
  • 心态调整:承认旧抽象的历史价值,但也要接受其当前的局限性。不要为了维护过去的投资而牺牲未来的可维护性。
  • 最终目标:通过“后退”来消除不必要的复杂性,使代码回归清晰,从而加速后续功能的开发。

意义与影响

这篇文章深刻揭示了软件工程中“过度设计”和“抽象强迫症”的危害。它挑战了传统的 DRY(Don't Repeat Yourself)原则的绝对性,指出在需求快速变化或理解不透彻时,盲目追求抽象反而会增加系统的耦合度和认知负荷。

对开发者的意义:

  1. 赋予重构的勇气:为那些面对复杂遗留代码感到无助的开发者提供了具体的操作指南和心理支持,鼓励他们打破对既有代码的迷信。
  2. 优化决策流程:提醒开发者在提取抽象前,需评估需求的稳定性。如果需求多变,保持代码的简单和重复可能比强行统一更划算。
  3. 提升代码质量:通过“内联-裁剪-重构”的方法论,帮助团队清理“条件代码”,使代码库更易于理解和测试。

对行业的影响: 这一观点与 Clean Code 和 Agile 开发中的许多理念相呼应,强调了代码的可读性和可维护性高于形式上的简洁。它促使团队反思代码评审的标准,不再仅仅因为“没有重复”而表扬代码,而是更关注抽象是否真正贴合当前业务逻辑。

附录:99 Bottles of OOP 第二版发布

文章末尾附带了 Sandi Metz 著作《99 Bottles of OOP》第二版的新闻。

  • 内容更新:新版包含 3 个新章节,篇幅比第一版长约 50%。
  • 多语言支持:为了涵盖更广泛的面向对象设计(而非特定语言),新版分别推出了 Ruby、JavaScript 和 PHP 三个版本。虽然技术内容相同,但示例代码使用了对应的编程语言。
  • 发行形式:提供 epub, kepub, mobi 和 pdf 格式,以及啤酒和牛奶两种“饮料”隐喻(指代不同的阅读体验或版本标识)。购买一次即可获得所有格式和语言版本的下载权限。
查看原文 →sandimetz.com