AI 时代的编程原则:DRY
Source: Dev.to
这是系列思考实验的第一篇文章,我在其中通过 AI 辅助开发的视角重新审视编程原则。显然,我们正处于范式转变之中,但没有人确定它会走向何方。因此,我并不试图预测未来;而是想在强假设情景下对假设进行压力测试,看看哪些能够站得住脚。在整个系列中,我们的判断基于更可能的预测——AI 将作为工具使用,而不是作为有感知的同事或乌托邦式的奇点之神。
DRY(Don’t Repeat Yourself)
DRY 来源于《The Pragmatic Programmer》(Hunt & Thomas,1999)。在实际使用中,人们把它当作一种 “提取方法” 的模式:一旦出现代码重复,就应该把它提取到一个公共位置以便复用。
很多人会把它与 Uncle Bob 在单一职责原则(Single Responsibility Principle)中的 single responsibility 视角相联系,而细读《Clean Architecture》的人会发现其中的矛盾;不过我们会在后续文章中讨论这个问题。
DRY 的原始定义比大多数人想象的更宽泛:
“系统中每一条知识必须有唯一、明确、权威的表示。”
为了本文的阐述,需要强调的是,这一原则的核心是 知识重复,而不是 代码重复。代码重复是知识重复的结果,而不是其原因。
为什么 DRY 很重要
DRY 解决了一个特定的问题:人类在追踪重复项时不可靠。
你在三个服务中都有相同的税务计算。需求变更后,你更新了其中两个。第三个仍然保持错误,直到四个月后有客户投诉才被发现。
抽象层面的做法是为了解决开发者会忘记复制位置的事实。
换句话说,DRY 是一种 上下文管理策略。人类开发者的思维上下文是有限的:你能记住的内容、屏幕上显示的内容、最近审阅的内容。DRY 通过保证每条知识只存在于唯一位置,减少了你需要保持的上下文量。如果没有复制,就不需要记住复制在哪里。
这种框架很重要,因为它重新定义了我们要回答的问题。
问题不是“重复是否不好?”(答案是肯定的)。问题是:“维护代码的实体是否需要 DRY 来管理其上下文?” 这完全取决于该实体拥有多少上下文。
当 DRY 出错时
在我们把 AI 引入之前,DRY 在被过度激进地应用时已经有众所周知的失效模式。这值得探讨,因为它表明即使对人类来说,这一原则也有其局限性。
示例:电子邮件验证
// Service A
public bool ValidateEmail(string email)
{
return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
// Service B – identical
public bool ValidateEmail(string email)
{
return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
一次 DRY 重构将其提取到共享库中:
// SharedLib.Email
public static bool ValidateEmail(string email)
{
return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
六个月后:
- Service A 需要接受加号地址(
user+tag@domain.com)。 - Service B 明确 不 接受加号地址,因为它是一个反欺诈流水线的一部分,加号地址会被视为红旗。
现在你必须在参数膨胀、策略模式,或放弃并再次复制之间做选择。共享库为你带来了六个月的一致性,随后却变成了 耦合负担。
这就是看起来相同的代码被当作相同知识来处理时会发生的情况。两个服务之所以拥有相同的代码是偶然的,而不是因为它们共享同一业务规则。有些人称之为“错误抽象”或“偶然重复”,在微服务架构中,由于服务以不同速度演进,这种情况比人们承认的更常见。
AI‑Assisted Duplicate Management
假设你的开发环境配备了 AI 工具,能够为代码库构建 语义索引,不仅映射语法上相同的代码,还能识别功能上等价的代码。当你修改其中一个实例时,它会找出所有相关实例并生成 上下文适配的差异,为每个受影响的服务打开 PR,且更改会根据该服务的特定约束进行调整。
在这种情况下,电子邮件示例的处理方式会有所不同。AI 能识别出所有三个验证器,明白它们在 + 地址 处理上有意不同,但共享相同的域名验证逻辑,并生成三个不同的补丁,更新域名检查 而不影响 + 地址的行为。每个服务保留自己的验证器。没有共享库。没有版本耦合。没有部署依赖。但在应该保持一致的部分实现了一致性。
AI 是否让 DRY 变得不那么必要?
乍一看,这似乎让 DRY 变得不那么必要。如果 AI 能在代码库中追踪并同步重复代码,为什么还要提取共享抽象?让每个服务各自拥有一份副本,由工具负责保持一致性。你会得到:
- 本地可读性
- 扁平的依赖图
- 独立部署
- 在 CI 时由 AI 验证的一致性
听起来很有吸引力。但这个论点存在结构性问题。
正是同样的 AI 能力在实现重复追踪的同时也 加速了代码生成。如果 AI 能够足够了解你的代码库以同步重复,它同样可以更快地生成新功能。开发者在更短的时间内交付更多代码。原本没人会手动编写的样板代码因为边际成本几乎为零而被生成。代码库因此膨胀。
而这里的论证出现了循环:随着代码库的增长,最终会 超出 AI 的上下文窗口。原本用于追踪重复的工具再也无法一次性看到所有重复。
这并非理论上的担忧。当前 LLM 的上下文窗口只有数万甚至数十万 token。一个大型微服务架构可能拥有数百万的…
(文章将在下一篇继续。)
上下文、DRY 与 AI
代码库的增长和 AI 上下文窗口如何重塑“不要重复自己”原则。
1. 核心张力
“即使有 RAG、嵌入和语义搜索,AI 同时能够推理的内容仍然有硬性上限。我们用来扩展有效上下文的每一种技术,都伴随着精度和可靠性的权衡。”
令人不安的认识是,反对 DRY 的论点本身是自我否定的:
- 那些削弱 DRY 的力量(更好的 AI、更大的上下文)同时也创造了加强它的条件(更多代码、更大的代码库、上下文溢出)。
2. 放大视角:上下文 vs. 代码库
问题: 上下文窗口的增长速度是否快于代码库的增长速度?
历史趋势很明确:
- 代码库已经增长了数十年。
- 每一次生产力提升——IDE、框架、包管理器、CI/CD,以及现在的 AI 辅助开发——都导致代码增多,而不是减少。
- 行业从未因生产力提升而写更少的代码;我们写更多、更快,以实现更宏大的目标。
3. 与计算资源的类比
| 资源 | 过去趋势 | 当前类比 |
|---|---|---|
| RAM 与应用内存 | RAM 增长 → 应用使用更多内存 | 上下文窗口增长 → 代码库消耗更多上下文 |
| CPU 速度与软件复杂度 | 更快的 CPU → 更复杂的软件 | 更大的上下文窗口 → 更大的代码库 |
| 网络带宽与数据量 | 带宽 ↑ → 数据量 ↑ | 上下文 ↑ → 需求 ↑ |
帕金森定律在软件领域的体现:代码会扩展以填满可供处理的上下文空间。
如果这一模式成立(且没有强有力的理由相信它不会继续),AI 将始终在其能够完全理解的边界附近运行。它在发现和追踪重复项方面会明显优于人类,但它并没有全知的奢侈。DRY 作为在有限上下文内保持代码库可管理的策略,仍然是相关的——无论上下文属于人还是机器。
4. 有哪些变化?
-
阈值移动。
- AI 能追踪的重复项比人类多,因此重复变得危险的临界点向外迁移。
- 小的实用重复(例如字符串格式化、基本校验)如果 AI 工具能够验证其一致性,可能就不值得抽取。
-
验证变为可能。
- 即使你选择重复,AI 也可以告诉你:
“这五个方法实现了相同的业务规则,其中两个已经出现漂移。”
- 这使得基于信息的决策成为可能,而不是通过生产故障才发现不一致。
- 即使你选择重复,AI 也可以告诉你:
-
原则的转变。
- 从“绝不重复” → “有意识地重复,并保持可见性”。
-
知识层面 vs. 代码层面的重复。
- Hunt 与 Thomas 在最初的阐述中区分了两者,但业界大多忽视了这一点。
- AI 工具现在可以在知识层面强制 DRY,同时容忍代码层面的重复——这或许正是该原则一直想表达的含义。
5. 实践指南
当你准备抽取共享方法或创建共享库时,问自己两个问题:
-
业务概念问题
我之所以抽取,是因为这些东西真正代表相同的业务概念,还是因为代码现在看起来相似?
-
上下文管理问题
我之所以抽取,是因为抽象真的让设计更好,还是因为我需要一种机制在有限的上下文中保持同步?
- 第一个问题始终是正确的提问。
- 第二个问题是 AI 改变计算方式的地方——逐步出现,并且仅在工具实际可见的范围内起作用。
6. 结论
就目前而言,DRY 仍然是务实的默认选择——并不是因为它是一条不可动摇的铁律,而是因为在有限的上下文(无论是人类还是机器)中,它仍然是保持代码库可维护性的最佳实践。
cred 原则,但因为 上下文,无论是人类的还是人工的,始终是有限的。只要它是有限的,我们就需要在其限制范围内工作的策略。AI 改变了复制的 阈值 和 可见性,但并未消除对深思熟虑的抽象的需求。