跳到主要内容

DRY原则

什么是DRY原则

DRY(Don't Repeat Yourself)原则是由Andy Hunt和Dave Thomas在《程序员修炼之道》一书中提出的软件开发原则。其核心思想是:

系统中的每一个功能都应该有唯一的、权威的、明确的表述。

这个原则强调:

  • 避免代码重复
  • 避免知识重复
  • 避免意图重复

常见误解

  1. 过度追求代码复用
    • DRY不等于消除所有重复的代码
    • 重点是消除知识和意图的重复
    • 有时适度的代码重复反而有利于系统维护

以下是一个过度追求代码复用的例子:

// 过度追求复用的错误示例
public class CommonUtils {
public static BigDecimal calculate(String type, BigDecimal amount) {
if ("orderDiscount".equals(type)) {
return amount.multiply(new BigDecimal("0.9")); // 订单9折
} else if ("memberDiscount".equals(type)) {
return amount.multiply(new BigDecimal("0.9")); // 会员9折
}
return amount;
}
}

// 虽然代码看起来相同,但业务含义不同,强行复用反而增加了耦合

更好的做法是根据业务含义分开处理:

public class OrderService {
private static final BigDecimal ORDER_DISCOUNT = new BigDecimal("0.9");

public BigDecimal calculateOrderDiscount(BigDecimal amount) {
// 订单折扣可能会随营销策略调整
return amount.multiply(ORDER_DISCOUNT);
}
}

public class MemberService {
private static final BigDecimal MEMBER_DISCOUNT = new BigDecimal("0.9");

public BigDecimal calculateMemberDiscount(BigDecimal amount) {
// 会员折扣可能会根据会员等级变化
return amount.multiply(MEMBER_DISCOUNT);
}
}
  1. 混淆表象重复和实质重复
    • 表象重复:代码看起来相似,但背后的业务含义不同
    • 实质重复:代码不仅相似,且表达相同的业务含义
    • DRY原则针对的是实质重复

正确应用DRY原则

1. 识别真正的重复

在以下情况下,即使代码看起来相似,也不应该强行复用:

  • 不同的业务规则
  • 不同的变化方向
  • 不同的变化频率

下面是一个识别和处理真正重复的例子:

// 存在实质重复的代码
public class UserValidator {
public boolean validateUsername(String username) {
return username != null && username.length() >= 3 && username.length() <= 20;
}
}

public class ProductValidator {
public boolean validateProductName(String productName) {
return productName != null && productName.length() >= 3 && productName.length() <= 20;
}
}

// 提取真正的重复逻辑
public class StringValidator {
public static boolean validateLength(String value, int min, int max) {
return value != null && value.length() >= min && value.length() <= max;
}
}

// 使用提取的通用逻辑
public class UserValidator {
public boolean validateUsername(String username) {
return StringValidator.validateLength(username, 3, 20);
}
}

public class ProductValidator {
public boolean validateProductName(String productName) {
return StringValidator.validateLength(productName, 3, 20);
}
}

2. 选择合适的抽象层次

根据重复的类型选择合适的抽象方式:

  • 代码层面:提取公共方法、基类
  • 设计层面:使用设计模式
  • 架构层面:微服务、组件化

下面是一个选择合适抽象层次的例子:

// 抽象基类处理通用逻辑
public abstract class BaseEntity {
private Long id;
private LocalDateTime createTime;
private LocalDateTime updateTime;

// 通用的审计字段处理
public void prePersist() {
this.createTime = LocalDateTime.now();
this.updateTime = this.createTime;
}

public void preUpdate() {
this.updateTime = LocalDateTime.now();
}

// getter and setter
}

// 具体实体类
public class User extends BaseEntity {
private String username;
private String email;

// 特定于User的业务逻辑
}

public class Product extends BaseEntity {
private String name;
private BigDecimal price;

// 特定于Product的业务逻辑
}

3. 使用设计模式

合理运用设计模式来消除重复:

  • 模板方法模式:处理算法骨架重复
  • 策略模式:处理算法实现重复
  • 装饰器模式:处理功能叠加重复
  • 观察者模式:处理交互逻辑重复

以下是使用模板方法模式消除重复的例子:

// 使用模板方法模式处理导出功能
public abstract class DataExporter {
// 模板方法定义导出流程
public final void export() {
beforeExport();
doExport();
afterExport();
}

// 钩子方法,允许子类自定义前置处理
protected void beforeExport() {}

// 抽象方法,子类必须实现具体的导出逻辑
protected abstract void doExport();

// 钩子方法,允许子类自定义后置处理
protected void afterExport() {}
}

// Excel导出实现
public class ExcelExporter extends DataExporter {
@Override
protected void doExport() {
// Excel特定的导出实现
}
}

// PDF导出实现
public class PdfExporter extends DataExporter {
@Override
protected void doExport() {
// PDF特定的导出实现
}

@Override
protected void afterExport() {
// PDF导出后的特殊处理
}
}

4. 持续重构

  • 及时识别和消除重复
  • 保持代码的整洁和可维护性
  • 在添加新功能时注意是否引入重复

平衡代码重复和可维护性

1. 可以接受重复的场景

  1. 简单的重复

    • 代码量很小
    • 重复不会造成维护负担
    • 强行抽象反而增加复杂度
  2. 不相关的重复

    • 业务含义不同
    • 变化方向不同
    • 生命周期不同
  3. 临时的重复

    • 在重构过程中
    • 在原型开发阶段
    • 作为过渡方案

2. 不能容忍的重复

  1. 知识重复

    • 业务规则重复
    • 配置信息重复
    • 依赖关系重复
  2. 结构重复

    • 类层次结构重复
    • 模块组织重复
    • 架构模式重复
  3. 数据重复

    • 数据定义重复
    • 数据存储重复
    • 数据传输重复

最佳实践

  1. 理解业务含义

    • 深入理解业务逻辑
    • 识别真正的重复
    • 区分表象重复和实质重复
  2. 选择合适的抽象级别

    • 不过度抽象
    • 保持代码的可读性
    • 考虑维护成本
  3. 持续重构

    • 保持代码整洁
    • 及时消除重复
    • 控制技术债务
  4. 使用合适的工具和技术

    • 代码分析工具
    • 重构工具
    • 版本控制系统
  5. 建立团队规范

    • 代码审查标准
    • 重构决策流程
    • 技术债务管理
  6. 关注变化方向

    • 预测可能的变化
    • 评估抽象的价值
    • 权衡利弊得失

总结

DRY原则的核心是消除知识和意图的重复,而不是简单地消除代码重复。在实践中,需要:

  1. 正确理解DRY原则的本质
  2. 在消除重复和保持代码质量之间找到平衡
  3. 根据实际情况选择合适的抽象级别
  4. 持续关注和调整抽象决策