DRY(Don't Repeat Yourself)——由Andy Hunt和Dave Thomas在《The Pragmatic Programmer》一书中阐述的一项基本原则。它指出:系统中的每个知识部分都应该具有唯一、明确、权威的表示。根据The Pragmatic Programmer, 20th Anniversary Edition的数据,违反DRY会导致更改一个元素需要在数十个地方进行修改,而每一个遗漏的部分都会成为错误的来源。
要点
DRY(Don't Repeat Yourself)——一项开发原则,要求项目中每个知识元素只存储一次。这意味着每个逻辑、配置或元数据必须恰好存在于一个地方。
该术语由Andy Hunt和Dave Thomas于1999年在《The Pragmatic Programmer》一书中引入。作者将DRY定义为“每个知识部分在系统中应该具有唯一、一致的表示”。DRY的对立面——WET(Write Everything Twice)方法,其中重复被视为常态。
根据加利福尼亚大学戴维斯分校(2019)的研究,具有高代码重复率的项目在修复错误上花费的时间多42%。原因是开发者必须找到并修改同一片段的所有副本——而在手动搜索中,遗漏是不可避免的。
将DRY作为代码质量的标准来应用。如果您注意到同一个模式在项目中出现了三次——将其提取为抽象,不要等到第四次重复。
单一职责原则(SRP)来自SOLID,指出一个类应该只有一个变更原因。DRY更广泛:它不仅涵盖类,还包括数据、配置、文档甚至业务规则。SRP关乎职责边界,DRY——关乎复制的不可接受性。
在移动开发中,这一区别尤为明显。如果相同的业务规则(计算税额、日期格式化)在Android和iOS部分中重复——这违反了DRY,即使SRP在每个平台内部形式上得到了遵守。解决方案——将公共逻辑提取到共享模块(KMM、C++)中。
根据Google Android Architecture Guidelines(2023)报告,使用公共模块处理业务逻辑的团队在需求变更时,与跨平台重复逻辑的项目相比,错误数量减少37%。
重复——移动项目中技术债务的主要来源。每个代码副本都会创建一个隐藏的依赖:要改变行为,必须找到并更新所有副本。遗漏哪怕一个就意味着错误。
考虑一个经典场景:在Android应用中,日期格式化在三个不同的Activity中执行。当切换到新格式(例如ISO 8601)时,开发者修复了两个文件,忘记了第三个——用户就会看到旧格式的日期。应用的用户评分下降,而查找错误需要两倍的时间。
Google Research(2020)的研究显示:移动应用中68%的关键错误与重复代码的非同步修改有关。在生产环境中修复此类错误的成本比代码最初就是统一的情况高出4.5倍。
使用带有禁止copy-paste检测规则的静态分析器(Detekt、SwiftLint)。配置CI,使重复超过N行的pull-request在没有合理说明的情况下不得通过审查。
一个典型的反模式——复制RecyclerView适配器并做微小修改。开发者不是创建一个带有配置的通用适配器,而是为每个屏幕创建单独的类。通过提取公共基类进行重构可以将代码缩短30–50%。
// 重复:两个独立的适配器
class UserAdapter {
fun bind(item: User) { /* ... */ }
}
class ProductAdapter {
fun bind(item: Product) { /* ... */ }
}
// DRY重构:公共基类
abstract class BaseAdapter<T> {
abstract fun bind(item: T)
}
class UserAdapter : BaseAdapter<User>() { /* ... */ }
class ProductAdapter : BaseAdapter<Product>() { /* ... */ }
在第一个示例中,每个适配器重新实现了绑定机制。添加新逻辑(分析、日志记录)时,必须修改每个文件。基类消除了这种重复:公共逻辑存在于一个地方,特定逻辑存在于派生类中。
在iOS项目中,URLSession的配置——标头、超时、错误处理经常被重复。每个服务都使用重复的设置创建自己的会话。
// 重复:每个服务重新配置会话
class UserService {
let session = URLSession(configuration: {
let cfg = URLSessionConfiguration.default
cfg.timeoutIntervalForRequest = 30
cfg.httpAdditionalHeaders = ["Authorization": "Bearer ..."]
return cfg
}())
}
// DRY:统一的会话工厂
struct NetworkConfig {
static var session: URLSession {
let cfg = URLSessionConfiguration.default
cfg.timeoutIntervalForRequest = 30
cfg.httpAdditionalHeaders = ["Authorization": "Bearer ..."]
return URLSession(configuration: cfg)
}
}
将配置提取到统一的NetworkConfig中,确保所有服务使用相同的标头和超时。一个地方的更改会自动应用于所有请求——这降低了在更改API密钥或协议版本时出错的风险。
继承——消除重复的自然方式:公共逻辑被提取到基类,特定逻辑被提取到派生类。然而,在移动开发中,过度使用继承会创建难以维护的僵化层次结构。组合(依赖注入)——更灵活的替代方案。
Google I/O 2023:现代Android架构分析显示,76%的Google团队更倾向于组合而非继承来消除重复。建议不要使用包含十种方法的BaseViewModel,而是为每个业务操作创建单独的UseCase类,并在需要的地方注入它们。
除“is-a”关系外,在所有情况下选择组合。如果A类是B类的特化——继承是合适的。如果A只是使用B的功能——请使用组合。
工具类(Extensions、Helpers)——避免重复的最简单方法。典型的候选对象:日期格式化、电子邮件验证、单位转换、使用SharedPreferences/UserDefaults。
// DRY:统一的日期格式化函数
fun Date.toDisplayFormat(): String {
val sdf = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault())
return sdf.format(this)
}
// 在应用程序的任何地方使用
textView.text = Date().toDisplayFormat()
Date.toDisplayFormat()扩展名声明一次即可在整个项目中使用。如果需要将格式从“dd.MM.yyyy”改为“yyyy-MM-dd”——只需修改一个文件,而不是修改每个出现格式化的Activity或Fragment。这正是DRY的精髓。
多模块Android项目通常在每个build.gradle中重复依赖版本。解决方案——version catalog(libs.versions.toml),将所有版本集中到一个文件中。
根据Android Developer Documentation(2024),迁移到version catalog可将依赖冲突减少52%,并由于统一的修改点而加快构建速度。
在项目开始时或首次重组模块时实施version catalog。如果项目已经存在重复——分配一天时间进行迁移:这在下次更新库时将会得到回报。
过早抽象——初学者最常见的错误。开发者看到两行相似的代码,立即将它们提取到一个公共函数中。一个月后需求发生变化,公共函数被参数和标志位塞满——变得比原始重复更复杂。Rule of Three正是为了防止这一点:不要抽象只出现了一两次的东西。
Martin Fowler在《Refactoring(2019)》一书中建议:“代码重复并不总是坏事。知识重复才是坏事”。如果两行代码偶然相同但表达不同的概念——这不是重复,而是巧合。Rule of Three有助于将随机巧合与系统性重复区分开来。
在抽象之前,评估语义。具有相同含义的复制代码——违反DRY。含义不同但语法相似的代码——不需要抽象的巧合。
过度参数化发生在一个函数试图通过标志位和布尔参数覆盖所有可能场景时。这样的代码违反了SRP并变得难以阅读。症状:如果函数有两个以上的布尔参数——这是过度抽象的代码味道(code smell)。
与其使用带有useCache: Boolean标志位的单一函数,不如创建两个具有明确名称的独立函数:fetchFromNetwork()和fetchFromCache()。明确性比干枯的抽象更重要——这与KISS原则相呼应。
当函数达到3个以上布尔参数时,重构过度参数化。拆分为具有清晰名称的独立函数——每个调用都将成为自文档化的。
常见问题
DRY(Don't Repeat Yourself)——要求每个逻辑单元存储在一个地方的原则。如果相同的代码出现在项目的多个部分中——这违反了DRY。修正:将重复的逻辑移到单独的函数、类或模块中。
WET(Write Everything Twice)——DRY的对立面,其中重复被认为是可接受的。在WET项目中,相同的代码片段可以存在于五个副本中,当需求发生变化时,开发者单独修改每个副本。WET增加了错误风险并减慢了开发进度。
DRY在过早抽象时有害:当两个相似但语义不同的代码部分被强行合并到一个函数中时。这会创建复杂、参数过载的代码。Rule of Three有助于避免这个错误:只在第三次重复后才进行抽象。
在Android中,DRY通过version catalog(libs.versions.toml)、适配器的公共基类、ViewModel工厂和实用的Kotlin扩展来应用。建议将业务逻辑提取到共享模块(KMM)中,并使用View Binding来消除findViewById的重复。
在iOS中,DRY通过具有默认实现的协议、共享网络配置(NetworkConfig)、UICollectionView单元格工厂和具有共享业务逻辑的SPM包来实现。标准类型(Date、String、URL)的Extensions减少了格式化和验证的重复。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。