什么是DRY原则?
DRY(Don’t Repeat Yourself)原则是由 Andy Hunt 和 Dave Thomas 在《The Pragmatic Programmer》一书中提出的软件开发原则。
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. 系统中的每一项知识都必须有一个单一、明确、权威的表示。
DRY 原则的核心不仅仅是"不要复制粘贴代码",而是确保知识和逻辑在系统中只存在一处。
核心思想
知识的单一来源
DRY 原则强调的是"知识"的不重复,而非简单的"代码"不重复。知识包括:
- 业务规则:如"订单金额超过100元免运费"
- 数据结构定义:如用户模型的字段
- 算法逻辑:如价格计算公式
- 配置信息:如 API 地址、超时时间
// 违反 DRY:同一个业务规则在多处定义
class OrderService {
func calculateShipping(orderAmount: Double) -> Double {
if orderAmount > 100 { // 业务规则:100元以上免运费
return 0
}
return 10
}
}
class CartViewController {
func updateShippingLabel() {
if cartTotal > 100 { // 重复的业务规则!
shippingLabel.text = "免运费"
} else {
shippingLabel.text = "运费:¥10"
}
}
}
class CheckoutViewModel {
func getShippingFee() -> Double {
return totalAmount > 100 ? 0 : 10 // 又是重复!
}
}
// 遵循 DRY:业务规则只定义一次
struct ShippingPolicy {
static let freeShippingThreshold: Double = 100
static let standardShippingFee: Double = 10
static func calculateFee(for orderAmount: Double) -> Double {
return orderAmount > freeShippingThreshold ? 0 : standardShippingFee
}
static func isFreeShipping(for orderAmount: Double) -> Bool {
return orderAmount > freeShippingThreshold
}
}
// 所有地方都使用这个单一来源
class OrderService {
func calculateShipping(orderAmount: Double) -> Double {
return ShippingPolicy.calculateFee(for: orderAmount)
}
}
class CartViewController {
func updateShippingLabel() {
if ShippingPolicy.isFreeShipping(for: cartTotal) {
shippingLabel.text = "免运费"
} else {
shippingLabel.text = "运费:¥\(ShippingPolicy.standardShippingFee)"
}
}
}
DRY vs WET
WET 是 DRY 的反义词,常见的解释有:
- Write Everything Twice(什么都写两遍)
- We Enjoy Typing(我们喜欢打字)
- Waste Everyone’s Time(浪费大家的时间)
iOS开发中的DRY实践
1. 使用 Extension 复用代码
// 违反 DRY:多处重复的日期格式化代码
class OrderCell: UITableViewCell {
func configure(with order: Order) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
dateLabel.text = formatter.string(from: order.createdAt)
}
}
class MessageCell: UITableViewCell {
func configure(with message: Message) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
timeLabel.text = formatter.string(from: message.timestamp)
}
}
// 遵循 DRY:通过 Extension 复用
extension Date {
func formatted(as format: String = "yyyy-MM-dd HH:mm") -> String {
let formatter = DateFormatter()
formatter.dateFormat = format
return formatter.string(from: self)
}
var displayString: String {
return formatted()
}
var shortDisplayString: String {
return formatted(as: "MM-dd HH:mm")
}
}
// 使用
class OrderCell: UITableViewCell {
func configure(with order: Order) {
dateLabel.text = order.createdAt.displayString
}
}
class MessageCell: UITableViewCell {
func configure(with message: Message) {
timeLabel.text = message.timestamp.displayString
}
}
2. 使用协议和默认实现
// 违反 DRY:多个 ViewController 重复实现相同的加载逻辑
class UserListViewController: UIViewController {
var isLoading = false
let activityIndicator = UIActivityIndicatorView()
func showLoading() {
isLoading = true
activityIndicator.startAnimating()
view.isUserInteractionEnabled = false
}
func hideLoading() {
isLoading = false
activityIndicator.stopAnimating()
view.isUserInteractionEnabled = true
}
}
class OrderListViewController: UIViewController {
var isLoading = false
let activityIndicator = UIActivityIndicatorView()
func showLoading() {
// 完全相同的代码...
}
func hideLoading() {
// 完全相同的代码...
}
}
// 遵循 DRY:使用协议和默认实现
protocol LoadingPresentable: AnyObject {
var loadingIndicator: UIActivityIndicatorView { get }
var containerView: UIView { get }
}
extension LoadingPresentable {
func showLoading() {
loadingIndicator.startAnimating()
containerView.isUserInteractionEnabled = false
}
func hideLoading() {
loadingIndicator.stopAnimating()
containerView.isUserInteractionEnabled = true
}
}
// UIViewController 的便捷扩展
extension LoadingPresentable where Self: UIViewController {
var containerView: UIView { return view }
}
// 使用
class UserListViewController: UIViewController, LoadingPresentable {
let loadingIndicator = UIActivityIndicatorView(style: .large)
// showLoading() 和 hideLoading() 自动可用
}
class OrderListViewController: UIViewController, LoadingPresentable {
let loadingIndicator = UIActivityIndicatorView(style: .large)
// 同样自动可用
}
3. 使用泛型消除重复
// 违反 DRY:为每种类型写重复的网络请求代码
class UserAPI {
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) {
let url = URL(string: "https://api.example.com/users")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(APIError.noData))
return
}
do {
let users = try JSONDecoder().decode([User].self, from: data)
completion(.success(users))
} catch {
completion(.failure(error))
}
}.resume()
}
}
class ProductAPI {
func fetchProducts(completion: @escaping (Result<[Product], Error>) -> Void) {
// 几乎完全相同的代码...
}
}
// 遵循 DRY:使用泛型
class APIClient {
static let shared = APIClient()
private let baseURL = "https://api.example.com"
func fetch<T: Decodable>(_ endpoint: String) async throws -> T {
guard let url = URL(string: baseURL + endpoint) else {
throw APIError.invalidURL
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
throw APIError.invalidResponse
}
return try JSONDecoder().decode(T.self, from: data)
}
}
// 使用
let users: [User] = try await APIClient.shared.fetch("/users")
let products: [Product] = try await APIClient.shared.fetch("/products")
let orders: [Order] = try await APIClient.shared.fetch("/orders")
4. 配置集中管理
// 违反 DRY:配置信息散落各处
class NetworkManager {
let timeout: TimeInterval = 30
let baseURL = "https://api.example.com"
}
class ImageLoader {
let timeout: TimeInterval = 30 // 重复!
let baseURL = "https://cdn.example.com"
}
class AnalyticsManager {
let timeout: TimeInterval = 30 // 又重复!
}
// 遵循 DRY:集中配置
enum AppConfig {
enum Network {
static let timeout: TimeInterval = 30
static let apiBaseURL = "https://api.example.com"
static let cdnBaseURL = "https://cdn.example.com"
}
enum Cache {
static let maxMemoryCost = 50 * 1024 * 1024 // 50MB
static let maxDiskSize = 200 * 1024 * 1024 // 200MB
}
enum UI {
static let animationDuration: TimeInterval = 0.3
static let cornerRadius: CGFloat = 8
}
}
// 使用
class NetworkManager {
let timeout = AppConfig.Network.timeout
let baseURL = AppConfig.Network.apiBaseURL
}
5. 使用计算属性避免状态重复
// 违反 DRY:维护冗余状态
class ShoppingCart {
var items: [CartItem] = []
var totalPrice: Double = 0 // 冗余!需要手动同步
var itemCount: Int = 0 // 冗余!需要手动同步
func addItem(_ item: CartItem) {
items.append(item)
totalPrice += item.price * Double(item.quantity) // 容易忘记更新
itemCount += item.quantity
}
func removeItem(at index: Int) {
let item = items[index]
totalPrice -= item.price * Double(item.quantity) // 容易出错
itemCount -= item.quantity
items.remove(at: index)
}
}
// 遵循 DRY:使用计算属性
class ShoppingCart {
var items: [CartItem] = []
// 计算属性:总是从 items 计算得出,保证一致性
var totalPrice: Double {
items.reduce(0) { $0 + $1.price * Double($1.quantity) }
}
var itemCount: Int {
items.reduce(0) { $0 + $1.quantity }
}
var isEmpty: Bool {
items.isEmpty
}
func addItem(_ item: CartItem) {
items.append(item)
// 不需要手动更新 totalPrice 和 itemCount
}
func removeItem(at index: Int) {
items.remove(at: index)
// 自动保持一致性
}
}
常见误区
误区1:所有相似代码都要合并
// 这两个函数看起来相似,但它们代表不同的业务概念
func calculateOrderDiscount(price: Double) -> Double {
return price > 100 ? price * 0.1 : 0
}
func calculateMembershipDiscount(price: Double) -> Double {
return price > 100 ? price * 0.1 : 0
}
// 错误的 DRY:强行合并不同的业务概念
func calculateDiscount(price: Double) -> Double {
return price > 100 ? price * 0.1 : 0
}
// 问题:当订单折扣和会员折扣的规则各自变化时,你会陷入困境
正确做法:如果两段代码只是"恰好相同"而非"本质相同",就不应该合并。判断标准是:当其中一个需要变化时,另一个是否也应该跟着变化?
误区2:过度抽象
// 过度 DRY:为了消除简单的重复而引入复杂的抽象
protocol Stringifiable {
var stringValue: String { get }
}
extension Int: Stringifiable {
var stringValue: String { String(self) }
}
extension Double: Stringifiable {
var stringValue: String { String(self) }
}
// 实际上直接用 String(value) 更清晰
正确做法:如果抽象带来的复杂度超过了重复代码本身的问题,那就保留重复。
误区3:忽视上下文的 DRY
// 不同层级可能需要不同的数据模型
// API 层
struct UserDTO: Codable {
let id: String
let name: String
let email: String
let created_at: String // API 返回字符串
}
// 领域层
struct User {
let id: String
let name: String
let email: String
let createdAt: Date // 领域模型用 Date
}
// 这不是重复!它们属于不同的层级,有不同的职责
误区4:DRY 与性能的权衡
// 有时"重复"是有意义的,比如性能优化
class HighPerformanceRenderer {
// 内联展开可能比抽象调用更快
func renderOptimized() {
// 直接写具体逻辑,而非调用通用方法
// 在性能关键路径上,这是可接受的
}
}
DRY 的正确应用
Rule of Three(三次法则)
一个实用的经验法则:当代码第三次重复出现时,再考虑抽象。
// 第一次:直接写
func fetchUsers() { /* ... */ }
// 第二次:可以接受一定程度的重复
func fetchProducts() { /* ... */ }
// 第三次:是时候抽象了
func fetch<T: Decodable>(_ endpoint: String) async throws -> T { /* ... */ }
判断是否应该 DRY 的问题清单
在决定是否消除重复之前,问自己:
这是"恰好相同"还是"本质相同"?
- 如果业务规则变化时两处应该同步变化,则本质相同
抽象是否增加了理解成本?
- 简单的重复可能比复杂的抽象更好
变化的可能性有多大?
- 如果某段代码几乎不会变化,重复的危害较小
团队成员能否理解这个抽象?
- 过度聪明的代码比简单的重复更危险
与其他原则的关系
DRY 与 SRP
单一职责原则有助于实现 DRY。当职责明确时,相关的知识自然会集中在一处。
// SRP 帮助 DRY
class PriceCalculator { // 单一职责:计算价格
func calculate(items: [Item], discount: Discount?) -> Double {
// 所有价格计算逻辑集中于此
}
}
DRY 与 KISS
DRY 和 KISS 有时会冲突。过度追求 DRY 可能导致过于复杂的抽象,违反 KISS。需要在两者间找到平衡。
DRY 与开放封闭原则
消除重复通常需要引入抽象,而这正是 OCP 所提倡的。好的抽象既能消除重复,又能使系统对扩展开放。