当写代码、改代码和重构代码的成本开始下降过去为了减少重复、避免修改、提前抽象而形成的软件设计习惯是否还应该保持同样的权重从 DRY、代码复用、OCP 和 YAGNI重新审视 AI Coding Agent 时代的软件设计成本。这个系列文章想讨论什么问题过去一两年AI Coding Agent 的进化速度非常快。我的使用方式也从最早“让 AI 帮我写一个方法、补一段代码”逐渐发展绝大部分编码工作都直接交给 Agent 完成。这个过程中我开始遇到一个以前没有认真思考过的问题一些过去按照传统软件工程思想精心设计出来的架构、抽象和扩展机制在和 Coding Agent 协作时反而会变成额外的理解成本、修改成本甚至是人与 Agent 之间的沟通成本。经历过几次类似的情况之后我开始重新审视一些过去几乎不会质疑的经验判断我们为什么要遵循这些传统的软件工程原则它们当初究竟是在解决什么问题这些问题到了 AI Coding 时代仍然同样昂贵吗如果软件开发底层的成本结构正在发生变化那么过去被我们视为“最佳实践”的设计方式是不是也应该重新计算一次它们的收益和代价和一些不同领域的朋友讨论之后我越来越觉得这并不是某一条原则究竟“对不对”的问题而是一个值得系统讨论的话题。所以我想通过一系列文章把这些过去习以为常的软件设计思想重新拿出来审视一遍。当然这个系列里会有不少来自个人实践的主观判断。这个系列也理应顺应同行的讨论以及技术的发展去更新是随着对事物理解和技术发展一起迭代更新的鲜活的有生命的文字。我更想把这些问题抛出来和不同项目、不同技术栈、不同 AI Coding 实践经验的开发者一起讨论当越来越多的代码开始由 AI 生产我们究竟应该怎样设计和组织软件才能用更低的开发成本、更少的不必要复杂度把真正可用、可维护的软件做出来目录一、Slack 的一次“去复用”实践二、DRY 真正要消灭的也许不是重复代码三、代码复用从来都不是免费的四、Coding Agent 改变的是这笔账里最重要的一个变量五、OCP 与 YAGNI还要不要那么早为未来设计六、从 Design for Reuse到 Design for Change总结本文想讨论什么作为一个“训练有素”的软件工程师在 AI Coding 时代到来之前我也逐渐形成了一整套关于“好代码”的判断标准不要重复自己尽量复用稳定代码尽量少改为未来可能出现的变化提前留出扩展空间。DRYDon’t Repeat Yourself不要重复自己、OCPOpen/Closed Principle开闭原则、YAGNIYou Aren’t Gonna Need It不要为尚未出现的需求提前设计以及围绕代码复用形成的一系列设计思想都深刻影响了我过去对软件设计的判断。但现在回过头来看这些原则并不是凭空产生的。它们背后其实都对应着一个非常现实的成本结构写代码很贵修改代码更贵而大规模重构尤其贵。正因为如此我们才愿意通过复用减少重复实现通过抽象降低未来的修改范围通过 OCP 尽量避免反复进入稳定代码也会提前为未来可能出现的变化设计扩展点。很多我们今天认为理所当然的“好设计”其实都在以不同方式降低未来修改软件的成本。而 AI Coding Agent 正在改变的恰恰是这个成本结构。当搜索代码、批量修改、接口迁移、补充测试乃至大规模重构都开始变得越来越便宜时我们有必要重新审视一下过去为了减少重复、避免修改和提前适应未来而付出的复杂度今天是否仍然值得所以这篇文章并不是要否定 DRY、代码复用或者 OCP而是想讨论一个更基础的问题当代码生产和代码修改的成本发生变化以后我们是否应该从“Design for Reuse”逐渐转向“Design for Change”——不再把“少写代码、尽量复用”本身当成目标而是更加关注一个系统能否以更低的成本应对未来真实发生的变化一、Slack 的一次“去复用”实践2017 年Slack 做了一个非常符合传统软件工程直觉的决定。当时 Slack 的 iOS、Android、桌面等客户端分别维护自己的代码。不同平台经常需要重复实现相同或相似的业务逻辑同一个 bug 可能要在几个客户端里分别修复甚至连一些看似基础的行为在不同客户端之间都可能出现差异。于是 Slack 开始开发一套跨平台的 C 库 LibSlack希望把同步、缓存以及部分公共业务逻辑集中在同一个地方。从传统软件工程的角度看这几乎是一个标准答案。既然几套客户端做的是同一件事情为什么要写三遍、四遍如果能够把公共逻辑放在同一个地方既可以减少重复代码也可以保证不同客户端行为一致。以后出现 bug只需要修改一份代码看起来无论从 DRY、代码复用还是维护成本上都更加合理。但几年之后Slack 逐渐停止了 LibSlack 的开发。原因也很现实。桌面客户端因为缓存策略、构建系统等原因并没有真正采用这套共享库Windows Phone 又逐渐退出历史舞台真正共享 LibSlack 的主要只剩下 iOS 和 Android。更重要的是不同平台之间的差异开始越来越明显。它们拥有不同的生命周期、工具链、性能要求和平台能力一份统一实现并不天然意味着更简单。Slack 最终选择重新让不同客户端拥有各自的实现再通过一致性测试去保证关键行为不会偏离。这个案例真正有意思的地方不在于 Slack 最后证明了“复用不好”而是它把两个过去经常被绑定在一起的问题拆开了行为保持一致并不意味着代码必须共享同一份实现。这其实是一个很重要且容易混淆的问题。我们过去经常默认如果几个地方需要保持一致那么最可靠的方式就是让它们使用同一份代码。但 LibSlack 的例子说明**“行为的一致性”和“实现的一致性”并不是一回事。**共享代码是一种保证一致性的手段但不是唯一手段更不是天然成本最低的手段。而在 AI Coding Agent 开始参与大量代码修改之后这个区别可能会变得越来越重要。二、DRY 真正要消灭的也许不是重复代码DRY 的全称是Don’t Repeat Yourself通常翻译成“不要重复自己”。但在实际开发中它经常被进一步简化成一个更加直观的判断不要写重复代码。所以当两个地方出现十几行相似实现时一个训练有素的软件工程师很容易产生条件反射这段代码是不是应该抽出来但仔细想一下真正危险的重复很多时候并不是“代码长得一样”而是同一份知识出现了多个版本。假设系统里存在一条业务规则订单支付超过 30 天以后不能直接退款。现在这个“30 天”分别存在于退款服务、前端页面、客服后台和某个定时任务里。某一天业务规则从 30 天修改成了 45 天开发者修改了其中三个地方却漏掉了第四个。这时候真正的问题并不是“我们多写了四份判断逻辑”而是同一个系统内部出现了两个互相矛盾的业务事实。这是一种知识上的重复。它危险的地方在于同一个事实失去了唯一、权威的来源。但另外一种情况并不完全一样。例如退款流程和售后换货流程现在碰巧有十几行代码非常相似。它们都需要检查订单状态、读取用户信息、记录操作日志于是从代码层面看两个流程存在明显的重复。可它们今天相似并不意味着它们表达的是同一个业务概念也不意味着半年之后它们还会沿着同一个方向变化。这种重复更多只是实现上的重复。过去我们很容易把这两种情况放在一起只要代码看起来相似就认为应该抽象、应该复用。但“代码长得一样”和“它们本质上是同一件事情”其实是两个完全不同的判断。Sandi Metz 曾经总结过一种很常见的演化路径两个地方出现重复于是开发者提取了一个公共抽象后来新的需求和原来的抽象“差一点”于是增加一个参数再来一个场景又多一个条件随着调用方越来越多原来那个为了简化系统而存在的抽象逐渐充满特殊情况。最后重复代码确实消失了但系统得到的是一个越来越难理解、越来越不敢修改的公共模块。这也是为什么“错误的抽象往往比重复更加昂贵”这个观点一直很有生命力。回过头来看 Slack 的 LibSlack也可以从这个角度理解。不同客户端的确需要共享某些业务规则也确实需要保持行为一致但这并不意味着它们必须共享所有实现细节。Slack 最终仍然需要一个 Single Source of Truth——一个关于“什么行为才是正确的”的统一定义但它不再坚持 Single Implementation。换句话说知识上的重复并不总等于统一实现。有些东西必须只有一个真相但并不一定只能有一份实现代码。三、代码复用从来都不是免费的“写一次用十次”大概是代码复用最有吸引力的描述。它让复用看起来像一笔稳赚不赔的交易代码更少bug 只需要修一次新功能也不需要到处重复实现。相比于几份相似代码散落在不同地方一份公共实现似乎天然更容易维护。但这里容易忽略一件事情复用本身也在建立关系。假设两个业务模块原本各自拥有一段 50 行的逻辑。我们把它们抽成一个公共模块之后确实消灭了几十行重复代码但两个原本相互独立的业务模块也从此开始依赖同一个接口、同一套行为和同一个变化节奏。A 模块出现一个特殊需求公共模块可能因此需要增加一个参数公共模块一旦修改B 模块又必须重新确认自己有没有受到影响。如果第三个、第四个调用者继续加入这个公共模块就需要同时照顾越来越多不同的需求。所以Reuse is also a form of coupling——复用本身也是一种耦合。当然这并不意味着耦合一定不好。如果十个地方表达的是同一条税务规则、同一种权限模型或者同一个通信协议那么它们本来就应该一起变化。此时通过同一份实现建立耦合反而可以有效避免知识分叉。真正麻烦的是另一种情况我们仅仅因为两段代码在今天看起来相似就推断它们未来也应该一起变化。很多项目里的“万能组件”就是这样慢慢出现的。一开始两个调用方很相似所以抽成公共模块第三个调用方有一点差异于是增加一个参数第四个场景希望跳过某一步于是增加一个 boolean flag第五个场景又需要在中间插入一段逻辑于是再增加 callback 或 hook。几年之后重复代码确实没有了但留下的是一个所有人都需要理解、所有修改都可能影响很多调用方、最终谁也不愿意轻易碰的公共组件。所以衡量复用是否真正有价值重要的不是“减少了多少代码”而应该是它有没有降低未来变化的总成本如果十个调用方本来就会因为同一个业务规则而一起变化那么共享实现可以降低变化成本。但如果几个业务场景只是当前看起来相似未来却拥有不同的变化原因那么强行复用可能只是用更少的代码换来了更大的修改半径。从这个角度看Slack 后来采取的一致性测试方案就很有意思。它没有放弃“一致”而是把一致性的来源从“必须共享一份实现”逐渐转移到了“共享关于正确行为的定义”。不同客户端可以拥有不同代码但只要在相同条件下满足同一套行为要求它们依然可以保持一致。也就是说真正需要被共享的很多时候不是每一行代码的重复实现而是关于正确行为的知识或者行为本身。四、Coding Agent 改变的是这笔账里最重要的一个变量如果实现重复并不总是坏事为什么过去的软件工程仍然如此强调复用一个非常现实的原因是人维护重复代码真的很贵。假设同一个变化涉及二十个地方。一个开发者首先需要找到这二十个位置然后判断哪些真正受到影响再逐一修改检查有没有遗漏更新相关测试最后确认这些修改没有引入新的问题。这里面相当一部分成本并不是来自真正困难的业务判断而是来自搜索、定位、机械修改和验证。过去我们之所以愿意建立大量 abstraction一个很重要的目的就是提前消除这些未来成本今天多付出一点设计和理解上的复杂度以后变化时只需要改一个地方。在人工编码时代这往往是一笔很划算的交易。但 Coding Agent 正在改变的恰恰是这个变量。现在的 Agent 已经可以做跨文件搜索、分析调用关系、批量修改接口、完成 API migration、执行大规模重构再根据编译和测试反馈继续修正。过去一个开发者可能需要半天甚至几天去完成的机械性修改现在越来越可能变成一次 Agent 任务。这当然不意味着重复代码已经没有成本也绝不意味着以后可以随意复制粘贴。Agent 一样可能遗漏一样可能误解业务语义。在复杂系统里如果缺少清晰的约束和测试Agent 也可能非常高效地产生错误。但真正值得注意的是修改代码的边际成本正在下降。过去五份简单实现意味着未来大概率需要人工修改五次。为了避免这五次修改我们愿意提前创造一个公共 abstraction。但如果未来 Agent 可以一次找到这五份实现理解它们局部的差异分别完成修改再通过测试进行验证那么“保留五份简单、局部、容易理解的实现”和“维护一份高度通用、但需要理解大量上下文的公共抽象”之间哪一个长期成本更低就不再像过去那么显而易见。这也是我目前觉得最值得重新计算的一笔账。实现重复可能正在从一种几乎必须被消灭的结构性风险重新变成一种可以权衡的工程成本。而与此同时错误 abstraction 的成本并没有同步下降。一个错误的抽象仍然会隐藏真实的业务边界它依然会扩大一次修改的影响范围未来无论是人还是 Agent在修改之前都必须先理解这套复杂的通用模型。某种意义上过去我们是在用 abstraction 去解决一个非常昂贵的问题。但如果那个问题本身正在变得便宜那么 abstraction 就需要重新证明自己的价值。五、OCP 与 YAGNI还要不要那么早为未来设计**OCPOpen/Closed Principle开闭原则**强调“对扩展开放对修改关闭”。简单来说就是希望系统增加新能力时尽可能通过增加新的实现完成而不是反复进入已经稳定的代码中修改。**YAGNIYou Aren’t Gonna Need It**则从另一个方向提醒我们不要因为未来“可能”出现某个需求就在今天提前把它设计出来。这两个原则并不真正冲突但在实际项目里我们经常会不自觉地偏向前者。比如一个后台系统现在只有一个需求把报表导出成 Excel。按照一种非常典型的设计思路我们可能很快就会想到以后说不定还会支持 PDF、CSV所以最好不要直接写一个 Excel 导出功能而是先定义一个通用的Exporter接口再实现一个ExcelExporter。为了未来方便扩展我们甚至可能继续加上 Factory、Registry 或其他插件机制。从传统工程经验来看这种设计并不奇怪。问题在于当系统里只有 Excel 这一个真实实现时我们其实并不知道所谓“通用导出”究竟应该长什么样。我们可能会假设所有导出格式都有标题、表头、行和样式于是按照 Excel 的模型设计一个看起来很通用的接口。但等 PDF 需求真正出现时我们才发现 PDF 更关心页面布局、分页和排版等 CSV 出现时又会发现 CSV 根本没有“样式”这个概念它更多只是字段和原始数据。回过头看当初那个“通用的 Exporter”很可能只是把 Excel 的特点包装成了一个看起来通用的 abstraction。这里真正的问题不是多写了几个 interface也不是抽象本身有什么错误而是当我们只有一个真实实现的时候其实很难知道未来真正稳定的共同点是什么。所谓“为未来设计”很多时候并不是在总结已经存在的规律而是在猜未来会长什么样。等第二个、第三个真实需求出现之后情况反而完全不同。此时我们已经知道 Excel、PDF、CSV 到底有哪些真实差异也开始看见它们之间哪些东西值得共享哪些东西应该保持独立。这个时候产生的 abstraction往往会比只有一个实现时想象出来的 abstraction 更接近真实问题。过去我们不愿意等原因也很现实以后再重构太贵了。所以即使现在信息不足我们也愿意提前猜希望用一点今天的复杂度换取未来少动代码。而 Coding Agent 让另外一种路线开始变得更加可行先按照今天真实存在的需求写出一个足够简单、清晰的 Excel 导出等 PDF、CSV 真的出现以后再根据真实差异让 Agent 帮我们完成结构调整和大规模重构。这也是为什么我认为YAGNI 在 AI Coding 时代的权重可能反而会提高。它并不是鼓励我们“不设计”而是在提醒我们延迟一个设计决策本身也是一种价值因为未来的我们会拥有更多真实信息。过去延迟 abstraction 最大的问题是未来重构成本太高。如果这部分成本开始下降我们就有更多资格等到真正的 pattern 出现以后再抽象。所以 AI 最终未必会让软件里出现更多 abstraction。恰恰相反它也可能让我们更敢于晚一点抽象。YAGNI的原则不止会更多应用在软件开发的层面我认为对于数据库的设计也会有更深远影响这点后续会再开一篇文章来讨论。六、从 Design for Reuse到 Design for Change回到文章最开始 Slack 的案例。LibSlack 最初并不是一个错误决定。在当时的条件下Slack 的确面对多个客户端重复实现同一套逻辑、不同平台行为不一致的问题。共享代码是一个非常合理的工程选择。后来环境变化了。真正使用这套共享库的平台减少了不同客户端之间的差异变得更加明显共享 implementation 带来的成本开始超过它带来的收益于是 Slack 又调整了设计。就像几年前很多用uniapp的团队再研发到后期还是转为用原生语言开发一样。这恰恰说明了一件事情好的软件设计也许从来都不是找到一个“永远正确”的 abstraction然后再也不修改它。设计本身就是对当前成本结构、需求和未来变化的一次判断。我们过去特别强调复用性是因为代码生产和重复修改的成本都很高。在这样的环境里通过复用减少未来修改次数是一种非常自然的工程优化。但现在 Agent 正在降低其中一部分成本。于是我越来越倾向于认为比 Reuse 更稳定的设计目标应该是另一个词Changeability——系统应对变化的能力。当真实需求发生变化时这个系统是不是容易理解一次修改能不能被限制在合理的范围里修改之后能不能容易地验证正确性如果原来的 abstraction 不再适用我们能不能以较低的成本把它拆掉再建立新的边界从这个视角回头看DRY、代码复用、OCP 和 YAGNI 都不应该被当成某种绝对正确的教条。它们只是解决不同问题的工具。当十个地方表达的是同一份业务知识时DRY 仍然极其重要当几个模块确实应该随着同一个业务原因一起变化时代码复用可以显著降低成本当系统已经存在稳定的扩展边界时OCP 仍然是非常有价值的设计思想。但当未来本身还不明确的时候YAGNI 也许应该获得比过去更高的优先级。Agent 时代真正需要改变的并不是简单地把某些传统原则判定为“过时”而是重新调整这些原则之间的权重。以后再看到两段相似代码时我们或许不应该条件反射地问“这里是不是违反了 DRY”更值得问的是它们表达的是不是同一份知识它们未来是否真的会因为同一个原因而一起变化把它们抽到一起是在减少未来的修改成本还是仅仅让今天的代码看起来更加整洁如果今天暂时允许一点实现重复等真正出现稳定模式后再重构它的代价到底还有多高这些问题比“有没有重复代码”更难回答但也更接近软件设计真正应该优化的东西。因为随着代码越来越便宜我们可能会越来越清楚地看到真正昂贵的从来不只是代码本身而是错误的边界、错误的耦合以及那些已经失去现实基础却因为重构成本太高而一直没人敢碰的 abstraction。过去我们经常问的是怎样才能少写一点代码而在 AI Coding Agent 时代也许更值得问的是怎样才能让未来真实发生的变化更便宜这两件事情看起来很像却并不是一回事而后者会更需要软件工程师对业务的熟悉度来做出判断这个后面也会再开一篇文章讲一下我认为的AI时代软件工程师的素质要求。总结如果你在短平快的时代已无法阅读长篇大论的文章那可以看这里。我目前的理解和判断是DRYDon’t repeat yourself 真正应该避免的是知识和业务事实的重复而不只是代码长得相似。代码复用并不是免费的。复用减少了重复实现但同时也会建立新的耦合关系。Coding Agent 正在降低搜索、修改和重构代码的成本因此实现重复的长期代价需要重新评估。当未来重构的成本下降以后YAGNIYou Aren’t Gonna Need It 的权重可能会上升我们可以更晚一点做抽象。比“最大化代码复用”更稳定的软件设计目标也许是 Changeability——让系统能够以较低成本应对真实变化。这并不是说传统软件工程原则已经过时。相反我更愿意把它理解成AI Coding Agent 正在改变这些原则赖以成立的成本结构。身处 AI Coding 时代我们需要做的不是抛弃过去的软件工程经验或者以原有的软件工程原则作为教条而是重新审视它们的适用边界、收益和优先级。对复用性的追求要逐步迁移到Changeability——系统应对变化的能力。这也是这个系列后面几篇文章想继续讨论的问题。