当前位置:首页>java>DRY 原则再思考:消除知识重复,而非代码相似

DRY 原则再思考:消除知识重复,而非代码相似

  • 2026-10-21 13:29:37
DRY 原则再思考:消除知识重复,而非代码相似
❝

DRY(Don’t Repeat Yourself) 并不是说“别写两行长得像的代码”,而是说:系统中的每一项知识(knowledge)都应当有且仅有一个权威、无歧义的表达

DRY原则由Andy Hunt与Dave Thomas在1999年的经典著作《The Pragmatic Programmer》中提出,其初衷并非追求代码的“简洁”或“短小”,而是对抗软件演进中最隐蔽的敌人——隐性重复。

当同一业务规则、配置逻辑或算法决策散落在多个角落时,每一次需求变更都可能变成一场“捉虫游戏”:你改了A模块,却忘了B模块也藏着同样的逻辑——于 bug在静默中滋生。

DRY 中的 “Yourself”是关键:它强调的是对自己所写系统的责任——不要让你自己(未来的你,或你的队友)陷入“一处修改、处处遗漏”的泥潭。DRY也是容易被误读的原则之一。很多人将其简化为“消灭视觉重复”,结果为了消除几行结构相似的 if 语句,硬造出泛型工具类、抽象基类甚至运行时反射机制,最终导致:

  • 代码耦合度上升;
  • 调试路径变长;
  • 业务语义被稀释。 
    区分 “知识重复” 与 “代码巧合相似” ——前者必须消除,后者不妨容忍。

一、DRY的含义:知识≠代码

把DRY简单理解为“不要写重复代码”是危险的简化。关键在于 “knowledge”(知识) —— 比如业务规则、算法逻辑、配置策略、状态转换条件等。而表面相似的代码,未必代表同一知识。

✅ 正确理解:

  • 如果两个地方的“重复”源于同一个业务规则(例如“用户必须年满18岁才能注册”),那么这个规则应该只在一个地方定义。
  • 如果两个地方只是“碰巧长得像”(例如两个独立模块都用了 if (list != null && !list.isEmpty())),强行合并反而会制造耦合。

DRY 的目标是:当需求变更时,你只需修改一处。


二、反面教材:违反 DRY 的 Java 代码

❌ 反例1:硬编码导致多处重复业务规则

// 用户服务publicvoidregisterUser(User user){if (user.getAge() < 18) {thrownew IllegalArgumentException("年龄必须大于等于18岁");    }// ...}// 订单服务publicvoidcreateOrder(Order order){if (order.getUser().getAge() < 18) {thrownew IllegalStateException("未成年用户不能下单");    }// ...}// 营销服务publicbooleanisEligibleForPromotion(User user){return user.getAge() >= 18; // 注意:这里逻辑一致,但表述不同!}

问题分析:

  • “18岁”这一业务规则散落在三个地方,且错误消息不一致。
  • 若将来法定成年年龄改为20岁,开发者必须找到所有相关位置修改,极易遗漏。
  • 这是典型的 “知识重复”,违反 DRY。

三、正面示范:正确应用 DRY

✅ 正例1:将业务规则封装为单一权威源

publicclassAgePolicy{publicstaticfinalint MIN_AGE_FOR_REGISTRATION = 18;publicstaticbooleanisAdult(int age){return age >= MIN_AGE_FOR_REGISTRATION;    }publicstaticvoidvalidateAdult(int age){if (!isadult(age)) {thrownew IllegalArgumentException("用户必须年满 " + MIN_AGE_FOR_REGISTRATION + " 岁");        }    }}// 使用publicvoidregisterUser(User user){    AgePolicy.validateAdult(user.getAge());}publicvoidcreateOrder(Order order){    AgePolicy.validateAdult(order.getUser().getAge());}

优点:

  • 业务规则集中管理,修改只需一处。
  • 方法命名清晰表达意图。
  • 常量命名具有业务语义(MIN_AGE_FOR_REGISTRATION),而非魔法数字。

四、警惕“伪 DRY”:过度抽象的陷阱

DRY 的最大误区,是把视觉重复当作知识重复。反例“为了不重复而制造复杂性”的典型错误。

❌ 反例2:强行通用化无关逻辑

// 试图“复用”两个完全无关的校验逻辑publicclassGenericValidator{publicstatic <T> voidvalidateNotNull(T obj, String name){if (obj == null) thrownew IllegalArgumentException(name + " 不能为空");    }publicstaticvoidvalidateListNotEmpty(List<?> list, String name){        validateNotNull(list, name);if (list.isEmpty()) thrownew IllegalArgumentException(name + " 不能为空列表");    }}// 在 UserValidator 中publicvoidvalidate(User user){    GenericValidator.validateNotNull(user.getName(), "姓名");    GenericValidator.validateListNotEmpty(user.getRoles(), "角色列表");}// 在 OrderValidator 中publicvoidvalidate(Order order){    GenericValidator.validateNotNull(order.getId(), "订单ID");    GenericValidator.validateListNotEmpty(order.getItems(), "商品列表");}

表面看:避免了 if (x == null) 的重复。实际上:

  • GenericValidator 成为“万能工具箱”,职责模糊。
  • 错误信息模板化,丧失上下文(“角色列表不能为空” vs “用户至少需分配一个角色”)。
  • 更严重的是:User 和 Order 的校验逻辑本无共性,强行统一反而掩盖了各自的业务语义。

✅ 正例2:接受“良性重复”,保持上下文清晰

publicclassUserValidator{publicvoidvalidate(User user){if (user.getName() == null || user.getName().isBlank()) {thrownew IllegalArgumentException("用户姓名不能为空或空白");        }if (user.getRoles() == null || user.getRoles().isEmpty()) {thrownew IllegalArgumentException("用户必须至少拥有一个角色");        }    }}publicclassOrderValidator{publicvoidvalidate(Order order){if (order.getId() == null) {thrownew IllegalArgumentException("订单ID缺失,无法创建");        }if (order.getItems() == null || order.getItems().isEmpty()) {thrownew IllegalArgumentException("订单中必须包含至少一件商品");        }    }}

为什么这是更好的做法?

  • 每个验证消息都贴合具体业务场景,便于用户理解和日志排查。
  • 类职责单一,符合 SRP(单一职责原则)。
  • 即使有几行结构相似的 if,但表达的是不同知识,不应合并。
❝

记住:重复的代码片段 ≠ 重复的知识。


五、复杂场景:DRY 与配置、策略模式的结合

当真正的“同一知识”需要在多个上下文中使用时,可通过策略、配置或领域模型实现优雅复用。

✅ 正例3:用策略模式统一费率计算(同一业务规则)

publicinterfaceShippingRateCalculator{BigDecimal calculateShippingCost(Order order);}@ComponentpublicclassDomesticShippingCalculatorimplementsShippingRateCalculator{@Overridepublic BigDecimal calculateShippingCost(Order order){// 国内运费规则(可能很复杂)return applyWeightBasedRate(order.getTotalWeight());    }}@ComponentpublicclassInternationalShippingCalculatorimplementsShippingRateCalculator{privatefinal DomesticShippingCalculator domesticCalc;publicInternationalShippingCalculator(DomesticShippingCalculator domesticCalc){this.domesticCalc = domesticCalc;    }@Overridepublic BigDecimal calculateShippingCost(Order order){// 国际运费 = 国内基础运费 + 固定附加费return domesticCalc.calculateShippingCost(order).add(new BigDecimal("20.00"));    }}

这里,国内运费的计算逻辑是共享知识,通过依赖注入复用,既避免重复,又保持扩展性。


六、DRY 与 YAGNI 的平衡

DRY 常与 YAGNI(You Aren’t Gonna Need It) 产生张力:

  • 过早抽象(违反 YAGNI):只为“可能”复用而设计通用组件。
  • 放任重复(违反 DRY):明知是同一规则却到处复制粘贴。

平衡之道:

  1. 先容忍少量重复,直到第二次或第三次出现时再重构。
  2. 问自己:“如果这个规则变了,我需要改几个地方?” 如果答案 >1,就该 DRY 了。
  3. 优先提取方法,而非类或接口。局部方法复用成本最低。

DRY是手段不是目的

DRY原则的终极目标不是“代码行数最少”,而是降低系统认知负荷与变更成本。这意味着:

  • 对真正的业务知识,确保单一权威源;
  • 对偶然相似的代码,宁可重复,也不制造虚假抽象;
  • 用清晰的命名与上下文,让每一行代码都讲述自己的故事。

重复不可怕,可怕的是用错误的抽象掩盖了本应独立的逻辑。保持警惕,保持简单,保持对“知识”而非“代码”的敬畏——这才是 DRY 的真谛。

信条:好的复用,是让变化只发生在一个地方;坏的复用,是让 bug 出现在所有地方。

● SpringBoot 优雅停机演进:实现原理与最佳实践

● 这有可能是OpenFeign最实用的用法与实践总结

● 盘点2025年java生态热门开源项目榜单

●简单即可靠:KISS原则在开发中的实践与误用

最新文章

随机文章