DRY (Don't repeat yourself),是敏捷开发的核心设计原则之一。DRY 原则规定,对于每个知识点,系统中都只有一个明确而权威的表示。这个原则倡导单一事实来源(single source of truth) 的哲学,适用于所有的软体开发工作,包含文件、设计、测试。但此原则常常被误解,不少人认为只要两个程式片段长得一样就是违反了 DRY 原则。事实上,有些情况中的重複并不是一件坏事,甚至有些没有重複的程式却违反 DRY 原则。本文将探讨 DRY 原则的运用情境。
违反 DRY 的案例
Copy & Paste
如果这么做的意图是因为偷懒,或是没有正当理由的 copy & paste,其实就没什么好讨论的了,确实违反 DRY 原则,应该尽快改善。常见的重构手段如 Extract Method
, Form Template Method
, Extract Class
。如果是有理由的 copy & paste,重複一两次也许能勉强接受,而到底需不需要重构,依然得依专案性质或团队讨论而定。例如,有个 class 或模组被3个专案引用,而引用的方式是 copy & paste,那到底该不该把这个 class 或模组抽出来呢? 这个问题没有标準答案,但我认为事不过三,超过3个就是一个警示了,应该尽快移除重複,否则日后维护成本(技术债)会非常可观。
知识的重複
例如某购物网站,网站 VIP 会员享有9折优惠,因此有关于计算金额的部分,都需要套用此逻辑,例如购物车、订单中的金额:
public class Cart { // 计算购物车金额 if (user.isVip()) { total = total * 0.9; } // ...}
public class Order { // 计算订单金额 if (user.isVip()) { total = total * 0.9; } //...}
很明显,网站 VIP 会员享有9折优惠
这个知识重複了,如果这个计算逻辑重複出现在许多 class 中,就确实违反 DRY 原则。因为,如果有新需求要将9折修改为8折时,就得在专案中一一更改,甚至很可能漏改。因此,正确的做法是将此逻辑抽出 calculateTotal
method,所有关于计算金额的地方都应该统一直接呼叫此 method:
public int calculateTotal(User user, int total) { return user.isVip() ? (int)(total * 0.9) : total;}
实作重複
例如购物网站专案中,某功能是判断购物车内是否有商品,但在专案里的 Cart
, CartService
出现两个看起来相似,实际上却相同的 method:
// Cartpublic boolean isEmpty() { return this.items.size() == 0;}
// CartServicepublic boolean isCartEmpty(Cart cart) { List<Item> items = cart.getItems(); return items.isEmpty();}
虽然名称不同,但背后的逻辑、意图是一模一样的。这个问题很有可能是因为工程师之间因为没有沟通好,导致他们都实作了重複的东西。除了事前充分讨论,也可以透过 code reivew 来避免这种问题。
另一个经典例子就是 自己造轮子,有些常见的功能可以直接引用可靠、开源的第三方 library,不必再做一个一模一样的东西。例如会员验证功能,可以用 JWT, Spring Security 等方案,不需要自己再实作一个验证功能。通常开源、可靠的 library 都是千锤百鍊、优化过的。如果自己再做一个同样的东西,通常不会比较好,反而可能造出「方的轮子」甚至是「弊帚自珍」症候群 (Not Invented Here Syndrome)。
未违反 DRY 案例
再次强调,DRY 原则并不是单纯的消除重複的程式码(Duplicated Code),而是知识、意图的重複。适度的重複程式码,反而不是个坏事。例如订单与购物车两个类别 Order
, Cart
有共同的属性 items
, total
。
public class Order { private List<Item> items; private int total; private LocalDatetime time; // ...}public class Cart { private List<Item> items; private int total; private boolean isExpired; // ...}
有些人可能会将共同属性抽出并建立父类别 Data
:
public class Data { protected List<Item> items; protected int total;}public class Order extends Data { private LocalDatetime time; // ...}public class Cart extends Data { private boolean isExpired; // ...}
这样做虽然减少了重複程式码,但这并不是 DRY 原则的最佳实践,因为修改后的程式反而变得较不直观,不能一目了然。在 DDD 的观点,这两个类别属于不同的两个 Entity,拥有不同的 domain knowledge,因此将他们独立开会比较适合。
结论
DRY 原则是软体开发中常用的最佳实践之一,它能够减少软体中的错误和混淆,并且使软体更易于维护和扩展。
DRY 原则所指出的论点并不仅仅是程式码的重複,更正确地来说是指知识、意图上的重複。有些重複的程式,没有违反 DRY 原则;有些不重複的程式,却违反 DRY 原则。而盲目追寻 DRY 原则可能适得其反,导致阅读程式时变得更不直观。有些时候,我们不要被设计原则的名词所蒙蔽了,其背后的思想才是我们真正该学习的。
References
本文转录自我的部落格 https://kaisheng714.github.io/articles/dry-principle