DRY 是我遇到的第一个编程原则,并且可能是我作为开发人员的第一年所知道的唯一一个。它也可能是最容易理解的原则之一。如果您在代码中看到两件事是相同的,也许它们应该只是一件事。很难与之争论。但是,我认为 DRY 就像其他所有原则一样 – 它有它的位置,但最好适度使用。而且我认为,由于它的普遍性和简单性,我们倾向于将 DRY 走得太远,太频繁了。

因此,事不宜迟,让我们深入探讨我对 DRY 的三个批评。

DRY被误用以消除巧合重复

有时事情碰巧是一样的,但这只是巧合。例如,考虑一些从虚构 API 请求披萨的 Python 代码。

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)

这些有效载荷中发生了很多重复。真正的比萨饼之间的唯一区别是浇头。人们会很想“干掉它”并进行以下重构:

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"])

问题是这两个披萨恰好有相同的外壳、酱汁和奶酪。如果我们开始使用两种具有不同外壳/酱汁/奶酪的披萨类型,我们永远不会进行这种重构。我们的代码不是围绕如何抽象地制作比萨饼的概念来构建的,而是它的架构与我们碰巧正在处理的这两个比萨饼的特定需求紧密耦合。我们将这段代码恢复原状的机会非常高。

DRY 创建可重用性假设

想象一下,我们在一家拥有庞大代码库的公司,其中多个产品领域想要整合订购披萨。与其让每个产品都编写自己的make_pizza()函数,不如将它放在common任何产品都可以导入和调用的库中?

所以我们沿着这条路走下去,我们最终得到了 5 个产品,每个产品都make_pizza()为他们想要的各种类型的比萨饼调用不同的参数数组。

现在出现了一些尖端产品团队,他们真的想开始制作一半夏威夷、一半意大利辣香肠的比萨。这个团队的开发人员都是关于 DRY 的,并且知道有一个很棒的共享披萨功能,所以他们去使用它。唯一的问题是,它不能接受拆分披萨订单。必须进行一些修改。

# 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 让我们陷入一种总是寻求重用代码的心态,即使它显然让我们走上了一条糟糕的道路。当我们真的应该假设重复时,我们最终会假设可重用性。

DRY 是一种通向不必要复杂性的药物

如果您是 10 倍开发人员,那么此时您可能已经列出了一长串针对我强调的问题的潜在解决方案。你可能会说我故意让我的例子变得迟钝来赢得我的观点,实际上我可以通过一些方法来解决这些问题。

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

所有这些想法都很棒。但请记住,这里的基本目标是使用单个 JSON 对象发送 POST 请求。这是一件非常非常简单的事情。现在我们正在谈论各种花哨的编程东西来尝试解决仅存在的问题,因为我们不想在少数不同的地方重复相同的 6 行代码段,因为 DRY 告诉我们这很糟糕。

正在发生的事情是,我们对 DRY 的坚持正在引导我们走上一条花园之路,以构建一个可以非常简单地编写的不必要的复杂应用程序。我认为这也经常发生。复制和粘贴几行代码几乎是零思考,没有时间。如果我们开始关心,查找和替换非常适合以后查找重复的内容。一旦我们开始思考如何避免复制粘贴和重构的思维过程,我们就输掉了复杂性之战。

小结

好吧,显然我并不是说我们应该将 DRY 完全扔出窗外。我不确定是否真的可以编写“永不重复”的代码。但我确实认为我们应该缓和对包含多次重复代码块的 PR 的下意识反应。至少在某些情况下,这可能是正确的做法。