软件设计

真正理解 DRY 原则:比代码重复更可怕的是错误的抽象

做代码审查时,我经常会遇到一个特别常见的场景。

3 分钟阅读
真正理解 DRY 原则:比代码重复更可怕的是错误的抽象 封面图

做代码审查时,我经常会遇到一个特别常见的场景。

为了修复一个功能进行搜索,结果发现几乎一模一样的代码出现在三个地方。只修复一处,另外两处就会继续作为 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。

把两段看似相似的代码强行合并,会发生以下情况。

  1. 创建一个公共函数
  2. 一方的需求发生变化,于是添加一个选项参数
  3. 另一方也发生变化,于是添加一个 if 分支
  4. 不知不觉间,变成一个有五个参数、谁也看不懂的函数

到了这一步,还不如保留两份复制的代码。

强行合并的函数崩溃的四个阶段
强行合并的函数崩溃的四个阶段

所以在实际工作中,人们经常使用“三次法则(Rule of Three)”。同一个模式出现两次时先观察,第三次出现时再进行抽象。重复三次左右后,就能看出它到底是相同的知识,还是纯粹的巧合。


我的判断标准

发现重复时,我会问自己一个问题。

这两段代码会因为相同的原因发生变化吗?

如果会因为相同原因变化,那就是真正的重复,应当合并。如果会因为不同原因变化,那么无论外观看起来多么相似,都应该保持原样。

合并前,只需要问这一个问题
合并前,只需要问这一个问题

今天的总结

把 DRY 原则提炼成重点,就是以下几点。

  • 重复的单位不是代码形状,而是知识(业务规则)。
  • 只合并会因为相同原因变化的代码。只是碰巧相似的代码,就保持原样。
  • 如果不确定,就使用三次法则。等到第三次重复时再抽象,也不算晚。

如果 KISS 是“保持简单”,DRY 就是“只把知识放在一个地方”。归根结底,两者都是为了降低变更成本。

你应该能想到一段每次搜索都会在三个地方出现的逻辑。那就是今天的重构候选。