DRY是一种被高估的编程原理 - gordonc


DRY是我遇到的第一个编程原则,可能也是我在成为开发者的第一年中唯一意识到的原则。它也可能是最简单的理解原则之一。如果你在你的代码中看到两件相同的东西,也许它们就应该是一件东西。这一点很难说得通。但是,我认为DRY就像其他的原则一样--它有它的位置,但最好是适度的。而我认为,由于它的普遍性和简单性,我们往往把DRY看得太重,太频繁了。

所以,废话不多说,让我们来看看我对DRY的三个批评。

1. DRY被误用来消除巧合的重复
有时事情恰好相同,但这只是一个巧合。例如,考虑一些Python代码从一个虚构的API请求一个比萨饼:

def make_hawaiian_pizza():
    payload = {
        crust: "thin",
        sauce:
"tomato",
        cheese:
"regular",
        toppings: [
"ham", "pineapple"]
    }
    requests.post(PIZZA_URL, payload)

def make_pepperoni_pizza():
    payload = {
        crust:
"thin",
        sauce:
"tomato",
        cheese:
"regular",
        toppings: [
"pepperoni"]
    }
    requests.post(PIZZA_URL, payload)

在这些有效载荷中发生了相当多的重复。实际上,这两个比萨饼之间唯一的区别就是配料的不同。我们很想把它 "DRY it up",并进行以下重构:

def make_pizza(toppings):
    payload = {
        crust: "thin",
        sauce:
"tomato",
        cheese:
"regular",
        toppings: toppings
    }
     requests.post(PIZZA_URL, payload)

def make_pepperoni_pizza():
    make_pizza([
"pepperoni"])

def make_hawaiian_pizza():
    make_pizza([
"ham", "pineapple"])

问题是,这两种比萨饼恰好有相同的饼皮、酱汁和奶酪。如果我们一开始就有两种比萨饼有不同的饼皮/酱汁/奶酪,我们就不会做这个重构。我们的代码不是围绕着抽象的比萨饼是如何制作的概念来架构的,而是紧紧地与我们碰巧要处理的这两种比萨饼的具体需求相联系。我们将这段代码放回原样的机会是非常大的。
(banq:业务领域模型与上下文有关,当你忽视上下文时,可能出现两种披萨相同)

2. DRY创造了一个可重用性的假设
想象一下,我们在一家拥有庞大代码库的公司,多个产品领域都想集成订购比萨饼。与其每个产品都编写自己的make_pizza()函数,为什么不把它放在一个任何产品都可以导入和调用的公共库中呢?

所以我们沿着这条道路走下去,最后有5个产品分别调用make_pizza()函数,用不同的参数数组表示他们想要的各种类型的比萨。

现在,一些前沿的产品团队来了,他们真的想开始制作一半是夏威夷,一半是意大利辣香肠的披萨。这个团队的开发者们都很注重干货,知道有一个很好的共享披萨函数,所以他们去使用它。唯一的问题是,它不能接受分裂的比萨饼订单。必须要做一些修改。

# cool_product/pizza.py
left_toppings = ["beef"]
right_toppings = [] 
make_pizza(left_toppings, right_toppings)  # this will be a very funny pizza 

# common/make_pizza.py
def make_pizza(*args):
    payload = {
        crust:
"original",
        sauce:
"tomato",
        cheese:
"regular",
    }
    if len(args) == 2:
        payload[
"toppings_left"] = args[0]
        payload[
"toppings_right"] = args[1]
    else:
        payload[
"toppings_left"] = args[0]
        payload[
"toppings_right"] = args[0]

    return requests.post(PIZZA_URL, payload)

这很有效,而且不需要改变API的每一种现有用法。不过,希望你能同意,这不是好办法。因为你传递了一个可选的第二个参数而改变第一个参数的含义是非常奇怪的。有许多其他的方法来做这个重构,但我断言,任何不修改make_pizza的现有调用或不为分顶披萨制作一个完全独立的函数(不是DRY)的改变都将是某种程度的糟糕。

你可能会认为合法合理的开发者却不会真的做这样的事情,而是会回到现有的调用,并修改它们以得到一个好的解决方案,但我已经看到这种情况到处发生。过度热衷于使用DRY会使我们陷入一种心态,即我们总是在寻找重用代码,即使它很明显地将我们带入一条坏的道路。我们最终会有一个可重用性的假设,而实际上我们应该有一个重复性的假设。

3. DRY是通往不必要的复杂性的关口
如果你是一个10倍的开发者,你可能在这一点上对我所强调的问题有一个长长的潜在解决方案清单。你可能会说,我是为了赢得我的观点而故意让我的例子变得晦涩难懂,实际上我有办法解决这些问题。

为了解决我的酱汁问题,也许我可以使用OOP风格,有一个PizzaOrderer类,可以为每个比萨饼类型进行子类化,允许每个类型覆盖合理的酱汁/面皮默认值。或者我可以用一个类来表示一个Pizza,并且有add_toppping()/add_topping_left()/add_topping_right()这样的方法,这样消费者可以在制作整个Pizza时快速添加配料,但也可以选择分割Pizza的颗粒度。还有很多其他的技巧,你可以建议。

所有这些想法都很好。但请记住,这里的基本目标是用一个单一的JSON对象发送一个POST请求。这是一件非常、非常简单的事情。现在我们正在谈论各种花哨的编程方法来试图解决这些问题,而这些问题的存在只是因为我们不想在很多不同的地方重复同样的6行代码,因为DRY告诉我们这样做是不好的。

现在的情况是,我们对DRY的坚持导致我们走上了一条花园式的道路,建立了一个不必要的复杂的应用程序,而这个应用程序可以写得非常简单。我认为这种情况也发生得太频繁了。复制和粘贴几行代码几乎不需要思考,也不需要时间。如果我们开始关心的话,查找和替换在以后找到重复的东西方面非常好。一旦我们开始思考如何避免复制粘贴而改用重构,我们就会输掉这场复杂的战斗。

banq:DRY不要重复自己是一种忽视上下文背景的愚蠢做法,特别是在业务领域中,任何模型都是有其有界上下文限制其适用范围i的,而DRY只是简单从表面上看是否重复,忽视了背后的上下文。上下文为王