做代码审查时,我经常会遇到一个特别常见的场景。
为了修复一个功能进行搜索,结果发现几乎一模一样的代码出现在三个地方。只修复一处,另外两处就会继续作为 Bug 留下。
今天的主角是正面针对这个问题的原则:DRY。它和上次介绍的 KISS 原则就像一对搭档,总是一起出现。
DRY 是“Don’t Repeat Yourself”的缩写,意思是不要在系统中重复相同的知识。
这个概念由 Andy Hunt 和 Dave Thomas 总结于 1999 年出版的书籍 <The Pragmatic Programmer> 中,原文定义相当准确。
所有知识在系统中都应该只有一个明确、无歧义且权威的表达方式。
这里值得注意的不是“代码”,而是“知识”这个词。这就是今天主题的核心。
为什么复制粘贴代码会有问题?
复制粘贴本身并没有错,问题会在之后发生。
如果相同的逻辑存在于三个地方,需求变更时就必须找到并修改全部三处。只要漏掉一处,它就会变成 Bug。
// 运费计算逻辑分散在三个文件中
// Cart.swift
let fee = total >= 50000 ? 0 : 3000
// Checkout.swift
let shippingFee = totalPrice >= 50000 ? 0 : 3000
// OrderSummary.swift
let delivery = price >= 50000 ? 0 : 3000
如果有一天收到“把包邮门槛降到 3 万韩元”的需求呢?你必须找到三个文件。由于搜索词也各不相同,肯定会漏掉其中一处。
// 将知识集中到一个地方
// Shipping.swift
enum ShippingPolicy {
static let freeShippingThreshold = 50000
static let fee = 3000
static func shippingFee(for total: Int) -> Int {
total >= freeShippingThreshold ? 0 : fee
}
}
这样一来,策略变更时只需要修改一个地方。这就是 DRY。
重复的不是代码,而是“知识”
不过,很多人会在这里踩坑。他们把 DRY 理解成“把所有看起来相似的代码都合并起来”。
DRY 所说的重复不是代码形状的重复,而是知识,也就是业务规则的重复。
这个区分很重要,因为世上存在只是碰巧相似的代码。
| 情况 | 算重复吗? |
|---|---|
| 运费计算逻辑出现在三个地方 | 真正的重复(相同知识) |
| 注册验证和活动报名验证碰巧相似 | 虚假的重复(不同知识) |
| 常量 3000 分别出现在运费和积分累计门槛中 | 虚假的重复(含义不同) |
注册验证和活动报名验证现在都要求“姓名必填、电话号码 11 位”,所以代码看起来一样,但两条规则会因为不同原因发生变化。如果把它们合并,修改活动规则时就可能导致注册功能出问题。
草率的抽象比重复更昂贵
所以在开发者社区中,经常有人说:“prefer duplication over the wrong abstraction”。这句话出自知名 Ruby 开发者 Sandy Metz。
把两段看似相似的代码强行合并,会发生以下情况。
- 创建一个公共函数
- 一方的需求发生变化,于是添加一个选项参数
- 另一方也发生变化,于是添加一个 if 分支
- 不知不觉间,变成一个有五个参数、谁也看不懂的函数
到了这一步,还不如保留两份复制的代码。
所以在实际工作中,人们经常使用“三次法则(Rule of Three)”。同一个模式出现两次时先观察,第三次出现时再进行抽象。重复三次左右后,就能看出它到底是相同的知识,还是纯粹的巧合。
我的判断标准
发现重复时,我会问自己一个问题。
这两段代码会因为相同的原因发生变化吗?
如果会因为相同原因变化,那就是真正的重复,应当合并。如果会因为不同原因变化,那么无论外观看起来多么相似,都应该保持原样。
今天的总结
把 DRY 原则提炼成重点,就是以下几点。
- 重复的单位不是代码形状,而是知识(业务规则)。
- 只合并会因为相同原因变化的代码。只是碰巧相似的代码,就保持原样。
- 如果不确定,就使用三次法则。等到第三次重复时再抽象,也不算晚。
如果 KISS 是“保持简单”,DRY 就是“只把知识放在一个地方”。归根结底,两者都是为了降低变更成本。
你应该能想到一段每次搜索都会在三个地方出现的逻辑。那就是今天的重构候选。

