DRY是一種被高估的程式設計原理 - gordonc

banq發表於2022-07-08

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只是簡單從表面上看是否重複,忽視了背後的上下文。上下文為王

相關文章