這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。在這篇博文中,我的目標是:- 揭示小函數的一些似是而非的優點- 解釋為什麼我個人認為有點不像建議說的那麼好- 解釋為什麼小函數有時是適得其反- 解釋一下我認為小函數在 mock 中真正有用的地方通常,編程建議總是說使用更優雅和有益的小函數。《Code Clean》被普遍認為是一本編程聖經,它有一章專門講述函數,文章的開始就是介紹一個非常長,令人頭疼的函數。該書認為該函數的最大問題是長度過長,並指出:它(函數)不僅長度太長,而且有多處重複的代碼,奇怪的字串,許多奇怪和不明了的資料類型和 API。三分鐘的學習後,你能瞭解函數的功能嗎?也許不能。那裡有太多的抽象層次。奇怪的字串,奇怪的函數調用混合在雙重嵌套,並由標誌位控制的 if 語句中。本章簡單地思考了什麼樣的特性會使代碼 “更容易閱讀和理解” 和 “允許任何一個讀者都能直觀地認識他們遇到的程式”,然後才說為了達到這個目的,必須將函數設定得更小一些。函數的第一條原則是必須小。函數的第二條原則是它必須更小。函數應該很小的觀點幾乎被認為是權威看法,不容質疑。在代碼審查,twitter 上,會議上,關於編程的書籍和播客中,關於代碼重構的最佳實務的文章中,等等。這個想法幾天前以這種推文的形式再次進入我的時間軸:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-1.jpg)Fowler 在他的推文中,連結了他關於函數長度的文章,並繼續指出:如果你不得不花費精力查看這一段代碼來確定它在做什麼,那麼你應該把它提取到一個函數中,並以它的功能命名該函數。一旦我接受了這個原則,我就養成了寫一些非常小的函數的習慣 - 通常只有幾行 [2](https://martinfowler.com/bliki/FunctionLength.html#footnote-nested)。任何超過半打行數的函數都會讓我覺得不舒服,對我而言,只有一行代碼的函數也並不罕見 [3](https://martinfowler.com/bliki/FunctionLength.html#footnote-mine)。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-2.jpg)有些人很迷戀小函數,所以對任何可能看起來很複雜的邏輯抽象成一個單獨的函數的想法向來都是推崇備至。我一直在研究人們繼承過來的程式碼程式庫,他們將這種觀點內化到完全扭曲的地步,以至於最終走向了不可挽回的地步,完全違背了這個觀點的最初意願。在這篇文章中,我希望解釋一下為什麼一些經常被吹捧的好處並不總是按照人們希望的方式發展,有的時候,一些觀點的應用會變得適得其反。## 小函數的好處(Supposed benefits of smaller functions)通常會列出一些理由來證明小函數背後的優點。### 只做一件事(Do one thing)![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-3.jpg)這個想法很簡單 - 一個函數應該只做一件事,並做好。從表面上看,這似乎是一個非常好的想法,跟 Unix 哲學不謀而合。當這個 ”一件事“ 需要被定義的時候,描述就變得模糊了。”一件事“ 可以是從簡單的返回語句到條件運算式,通過網路調用的數學計算(等等)。正常情況下,許多時候,這個 ”一件事“ 意味著對某些(通常是業務)邏輯的單級抽象。例如,在 Web 應用程式中,像 “建立使用者” 這樣的 CURD 操作可能是 “一件事”。通常,建立使用者至少需要在資料庫中建立記錄(並處理任何伴隨的可能錯誤)。此外,使用者註冊後可能還需要向他們發送歡迎電子郵件。另外,人們也可能希望可以自訂一個事件,像 kafka 這樣的訊息中介軟體,可將此事件發送給其他各個系統。因此,“單一抽象層次” 不僅僅是一個層次。我所看到的是,那些完全理解函數應該做 “一件事” 的想法的程式員往往很難抵制將遞迴應用於他們編寫的每個函數和方法中。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-4.jpg)因此,我們現在不再是為了可以被理解(和測試)而抽象成一個合理的單元,而是將更小的單元劃分出來,以描述 ”一件事“ 的每個組成部分,直到它完全模組化,完全 DRY(Don't repeat yourself)。## DRY 的謬論![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-5.jpg)DRY 和儘可能小的函數的傾向並不一定是同一件事,但我已經看到後者很多時候會讓目標變成前者。DRY 在我看來已經是一個很好的指導原則,但實用和理性在教條地堅持下犧牲了,特別是那些信服 Rails 的程式員。Python 的核心開發人員 Raymond Hettinger 發表了一篇名為 Beyond PEP8 的精彩演講:[那是美妙而易懂的最佳實務](https://www.youtube.com/watch?v=wf-BqAjZb8M)。這是一個必須關注的話題,不僅適用於 Python 程式員,也適用於任何對編程感興趣或以開發程式為生的人,因為它非常清楚地解釋了教條式遵守 PEP8 的謬誤,這是真正的 Python 風格指南,它介紹了很多底層實現。在 PEP8 上的談話焦點並沒有比可以應用的見解重要,(而且)其中許多情況是語言描述不了的。即使你沒有看完整個演講,你也應該看下這個講話的開頭一分鐘,這個演講與 DRY 的警鳴做了令人驚訝的類比。程式員堅持要儘可能多地精簡代碼,會讓他們只關注局部,忽略掉整體。我對於 DRY 的主要問題在於它強制抽象成為抽象 - 嵌套和太早的抽象。由於不可能完美地抽象,所以我們只能儘可能地做到足夠好的抽象。“足夠好” 的定義很難,並且取決於很多因素。在中,“抽象” 一詞可以與 “函數” 互換使用。例如,假設我們要設計抽象層 A,我們可能需要考慮以下幾點:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-function/1_Mh46Hv7CEkfVc_SKlA0d1w.png)- 支撐抽象概念 A 的假設性質以及它們可能持有的水平的可能性(以及可能持續多長時間)- 抽象層 A(抽象層 X 和抽象層 Y)以及建立在抽象層 A(抽象層 Z)之上的任何抽象層的抽象層在其實現和設計中傾向於保持一致性,靈活性,可擴充性和正確性。- 未來抽象(抽象層 M)的需求和期望可能建立在抽象 A 之上,以及可能需要在 A(抽象層 N)之下支援的任何抽象我們開發的抽象層 A 不可避免地會在未來不斷被重新評估,並且很可能會部分甚至完全失效。一個能夠支撐我們需要的,不可避免的修改的最重要的特徵就是設計我們的抽象,使之變得更靈活。儘可能最大限度地最佳化代碼意味著,將來需要適應修改時,(這將)剝奪了我們自己的靈活性。我們最佳化時也要做到讓自己有足夠的餘地來適應不可避免的變化,遲早會有這樣的要求,而不是馬上為了完美的契合而進行最佳化。最好的抽象是最佳化得足夠好,但不完美的抽象。這是一個函數,而不是一個錯誤。理解抽象的這種非常突出的性質是設計好程式的關鍵。Alex Martelli 是鴨子理論和蟒蛇派的名人,他著名的演講 “抽象塔” 中的投影片非常值得一讀。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-function/1_fvfBJ21qOdt3XGAFHa0oOg.png)![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-function/1_i5vRl8dA8docZutvy-LgYA.png)Rubyist Sandi Metz 有一場名為 All The Little Things 的著名演講,她認為 “重複比錯誤的抽象代價更低”,因此 “傾向於重複的抽象”。在我看來,抽象概念不可能是完全 “正確的” 或者 “錯誤的”,因為劃分 “正確” 與 “錯誤” 的界限本來就很模糊。實際上,我們精心設計的 “完美” 抽象,只是一個業務的要求或者被委託的一個錯誤的缺陷報告。我認為這有助於將抽象視為圖譜,如我們在本文前面看到的圖表一樣。該圖譜的一端最佳化精度,我們代碼的每個方面,最後都要求要精確。這當然有其好處,但是因為努力尋求完美的對齊,所以並不適合好的抽象。該圖譜的另外一端最佳化,帶來了不精確性和缺少邊界。雖然這確實允許最大的靈活性,但我發現這種極端的傾向將導致其他的缺點。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-6.jpg)跟其他大多數事情一樣,“理想模型” 處於這兩者之間。沒有一種娛樂能取悅所有人。這個 “理想模型” 也取決於許多因素 - 工程和社會關係 - 並且,良好的工程是能夠確定這個 “理想模型” 在不同環境中所處的位置,並能不斷地重新評估並校準這個模型。### 給抽象命名(The name of the game)說到抽象,一旦確定了抽象什麼以及如何抽象,就需要給它一個名稱。給事物命名向來都很難。這種方式(給抽象命名)普遍認為是編程過程中,使代碼能活得更長的有效辦法,更具描述性的名稱是一件好事,甚至有人主張用帶有注釋的名稱代替代碼中的注釋。他們的想法是,一個名稱越具描述性,意味著封裝得越好。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-7.jpg)這個觀點在 Java 的世界裡普遍存在,( Java 程式中)冗長的名稱非常常見,但我從來沒有發現這些冗長的名稱使代碼更加容易閱讀。例如,可能 4-5 行的代碼中就隱藏一個名字非常長的函數。當我正在閱讀代碼時,突然一個非常長的單詞出現,會讓我停下來,因為我得試圖處理這個函數名稱中的所有不同的音節,嘗試將它融入到我已建立的心智模型中,然後決定,是否通過跳轉到它定義的地方,來看它的具體實現。然而,“小函數” 的問題在於追尋小函數的過程中導致了更多的小函數,所有這些函數都傾向於在記錄自己和避免討論的過程中給出了非常冗長的名稱。結果,處理描述詳細的函數(和變數)的名稱帶來了認知的開銷,以及將它們映射到我迄今為止構建的心智模型中,以確定哪些函數需要深入探究,哪些函數可以剔除,並將這些拼圖拼在一起以揭開程式的面紗,但處理冗長的函數(和變數)名使得這個過程變得更加地困難。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-8.jpg)就我個人而言,與查看自訂的變數或者函數名相比,從視覺角度來說,我發現程式設計語言提供的關鍵字,構造和習慣用法更加容易接受。例如,當我閱讀 if-else 模組時,我很少需要花費精力去處理關鍵字 if 或者 elseif,只需要花時間理解程式的邏輯流程。一個 VeryVeryLongFuncNameAndArgList 名稱會中斷我的推理思路。當被調用的函數實際上是一個可以輕鬆內聯的單線程時尤其如此。環境切換很昂貴,不管是 CPU 環境切換還是程式員在閱讀代碼時不得不在思想上的環境切換。過度強調小函數的另外一個問題是,尤其是那些描述性很強但名字不直觀的函數,在程式碼程式庫中更難搜尋到。相比之下,一個名為 createUser 的函數很容易,且直觀地用於 grep,比如 renderPageWithSetupsAndTeardowns(在《Clean Code》中是作為明星例子,這個名字不是最容易記住的名稱,也不是最容易搜尋到的名字)。許多編輯器也對程式碼程式庫進行了模糊搜尋,因此具有相似首碼的函數也更可能造成搜尋時出現多餘的結果,這不是我們想要的。### 本地的丟失(Loss of Locality)(註:這裡指可以在本函數,本檔案,本包中實現的代碼,卻為了小函數移到了其他的函數,檔案,或包中)當我們不必跳過檔案或包來尋找函數的定義時,小函數的效果最好。“Clean Code” 一書為此提出了一個名為 “The Stepdown Rule” 的原則。** 我們希望代碼能像自上而下的敘述一樣容易閱讀。我們希望每個函數都被下一級抽象層次的人所遵循,以便我們閱讀程式,讀取函數列表時,可以一次下降一個抽象層次。我稱之為 “The Stepdown Rule"。**這個觀點理論上可行,但在實際的實踐中,卻很少能發揮作用。相反的,我看到的大多是在代碼中增加更多的函數,減少了本地代碼。讓我們以三個函數 A,B 和 C 的假設開始,一個調用另外一個。我們的初始抽象印證了某些假設、要求和注意事項,所有這些都是我們在最初設計時仔細研究和論證過的。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-function/1_EGR-6c3hu_6joqdNYHfyLQ.png)很快,假設我們有一個新的需求或一個附加功能的情況下,我們需要迎合沒有預見的或一個新的約束。我們需要修改函數 A,因為它封裝的 “一個整體” 已經不再有效(可能從一個開始就無效,現在我們需要修改它,使它有效)。按照我們在 《Clean Code》中所學到的,我們處理這些問題的最好辦法是,建立更多的函數,隱藏掉各種雜七雜八的新需求。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-function/1_N77X5FQhiscmnNUKlR8_Cw.png)我們按照我們的想法修改後,過個幾周,如果我們的需求又修改了,我們可能需要建立更多的函數去封裝所有要求增加的修改。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-function/1_aQn2iFAvxzrJr_89aTghtQ.png)再來幾次,我們就真正的看到了 Sandi Metz 在她的博文 [《The Wrong Abstraction》](https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction) 中描述的問題。這篇博文說:已經存在的代碼具有強大的影響力。它的存在表明它是正確和有效。我們知道代碼代表了付出的努力,我們非常積極地維護這個努力的價值。不幸的是,可悲的事實是,代碼越複雜,越難以理解,即設計它時投入越大,我們就越覺得要保留它(“沉沒成本謬論”)。如果還是同一團隊成員繼續維護它,我相信這是對的,但當新的程式員(或經理)獲得程式碼程式庫的所有權時,我會看到相反的結果。以良好意圖開始的代碼,現在變成了意大利麵條的代碼,代碼不再簡潔,成了地獄般的代碼,現在 “重構” 或者有時甚至重寫代碼的衝動更加誘人。現在,人們可能會爭辯說,從某種程度上說,這是不可避免的。他們是對的。我們很少討論編寫將會退役的代碼是多麼重要。過去我寫過關於使代碼在操作上易於退役的重要性,在涉及程式碼程式庫本身時更是如此。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-9.jpg)通常情況下,程式員只會在代碼確定被刪除,或者不再使用時,將代碼視為 “已死亡”。如果我們開始(以代碼將 “死亡”)思考我們的編寫的代碼,那麼每增加一個新的 git commit,我認為我們可能會更加積極地編寫易於修改的代碼。在思考如何抽象時,認識到我們正在構建的代碼可能距離死亡(正在被修改)只有幾個小時的事實對於我們很有協助。因此,為了便於修改代碼而進行的最佳化往往比試圖構建 《Clean Code》中提到的自頂向下的設計更好。## 類汙染在支援物件導向的編程裡,小函數帶來了更大或者更多的類。在像 Go 一樣的程式設計語言裡,我看到這種趨勢導致更大的介面(結合介面實現的雙重打擊)或者大量的小包。這加劇了將商務邏輯映射到我們已經建立的抽象認知的開銷。類/介面/軟體包的數量越多,一舉拿下就越困難,這樣做證明了我們構建的這些不同類/介面/軟體包所需要的維護成本(很大)是合理的。## 更少的參數較少函數的支援者幾乎總是傾向於支援將更少的參數傳遞給函數。函數參數較少的問題在於,存在依賴關係不清晰的風險。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-10.jpg)我已經看到了 Ruby 類有 5-10 個方法,所有這些方法通常會做一些非常簡單的事情,並且可能會有一兩個變數作為參數。我也看到他們中的很多人改變了共用的全域變數的狀態,或者依賴於沒有明確傳遞關係的單例,只要存在一種情況,就(跟我們之前的討論的)是一種相反的模式。此外,當依賴關係不明確時,測試將變得更加複雜,在針對我們的 itty-bitty 函數的獨立測試前,需要重新設定和修改狀態值,才能讓它運行。## 更難閱讀這已經在前面陳述過了,但值得重申的是 - 小函數的爆炸式增長,特別是一行的函數,使程式碼程式庫難以閱讀。這尤其會傷害那些代碼應該被最佳化的人 - 新手。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-11.jpg)程式碼程式庫中有幾種類型的新手。根據我的經驗,一個好的經驗法則是記住某些可能會檢查上述 ”新“ 類別的人。這樣做可以協助我重新評估自己的假設,並重新思考我可能會無意中將某些新手加入到第一次閱讀代碼的新手中。我意識到,這種方法實際上導致比其他方式可能更好更簡單的代碼。簡單的代碼不代表很容易編寫,而且也很少是 DRY 最好的代碼。它需要大量細心的思考,關注細節和小心翼翼地達到簡單的解決方案,是正確和水到渠成的。這種來之不易的簡單性最令人信服的地方在於它適合於新老程式員,易於理解的 ”舊“ 和 ”新“ 的所有可能的定義。當我對程式碼程式庫感到陌生時,如果我有幸已經知道其所使用的語言或者架構時,(那麼)對我來說最大的挑戰是理解商務邏輯或實現細節。當我不那麼幸運時,即面臨著(必須)通過用我外行的語言來編寫程式碼程式庫的艱巨任務時,我面臨的最大挑戰是能夠對語言/架構有足夠的理解,如履薄冰,以至能夠理解代碼在做什麼而不掉進坑裡,同時能夠區分出我需要真正理解的,跟目標相關的 ”單一事物“,以便在項目前進中取得必要的進展。在這段時間裡,我沒有看過一個陌生的程式碼程式庫,所以我會說:嗯,這些函數都是足夠小,並且符合 DRY 風格的。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-12.jpg)在我嘗試尋找問題答案的同時,我冒險進入了未知領域,真正希望的是讓最少數量的思維跳躍和環境切換。投入時間和精力,讓代碼未來的維護者或者消費者變得更容易(理解),這將會產生巨大的回報,特別對於開源項目。這是我希望自己在職業生涯早期做得更好的一件事,而且這段時間我都很注意(這一點)。## 什麼情況下小函數有意義當所有情況都考慮到了,我相信小函數絕對有它的意義,特別是在測試時。## 網路 I/O這不是一篇關於如何最好地為大量服務編寫函數,整合和單元測試的文章。然而,當談到單元測試時,網路 I/O 通過某某方式測試,好吧,實際上沒有測試。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-13.jpg)我不是 mock 函數的粉絲。 mock 函數有幾個缺點。首先, mock 是一些結果的人工類比。只有當我們的想象力和我們具備預測我們應用程式可能遇到的各種失敗模式的能力時。 mock 也很可能與他們所支援的真實服務不同,除非每個人都對真正的服務進行過嚴格的測試(註:對細節很瞭解)。當每個特定類比只有一個執行個體並且每個測試使用相同的類比時, mock 才是最好的。也就是說, mock 仍然是單獨測試某些形式的網路 I/O 的唯一方法。我們生活在一個微服務時代,並將大部分(如果不是全部的話)關於我們的主要產品的關注都外包給供應商。現在很多應用程式的核心功能都需要一個調用或者多個調用,對這些調用進行單元測試的最佳方法是將其類比出來。總體而言,我發現限制能夠 mock 的範圍,才是最好的。調用電子郵件服務的 API,以向我們新建立的使用者發送歡迎電子郵件,(當然這)需要建立 HTTP 串連。將此請求隔離到儘可能少的函數中,並允許我們在測試中 mock ,以最小化代碼量。通常,這應該是一個不超過 1-2 行的函數,用於建立 HTTP 串連並返回任何錯誤以及響應。將事件發給 Kafka 或在資料庫中新建立的使用者時也是如此。## 基於屬性的測試對於那些能夠通過這種小代碼提供如此巨大利益的東西,基於屬性的測試卻沒有被充分利用起來。(這種測試)由 Haskell 圖書館的 QuickCheck 發明的,並在 Scala(ScalaCheck)和 Python(假設) 等其他語言中被採用,基於屬性的測試允許人們產生大量符合給定測試規範的輸入,並斷言每一個情況的測試通過條件。許多基於屬性的測試架構都是針對函數的,因此將任何可能受到基於屬性測試的東西隔離到單一函數上是有意義的。我發現這在測試資料的編碼或解碼,測試 JSON 或 msgpack 解析時尤其有用。## 結論這篇文章的意圖既不是說 DRY 也不是說小函數本身就是壞的(儘管本文的標題給出了這樣的暗示)。只是說他們本質上沒有好壞。程式碼程式庫中的小函數的數量或平均函數長度本身並不是一個可以吹噓的指標。在 2016 年 PyCon 談話中有一個名為 onelineizer 的話題,講述了一個可以將任何 Python 程式(包括它本身)轉換為一行代碼的同名 Python 程式。雖然這使得會議討論變得有趣而誘人,但在相同的問題上編寫(類似的)產品代碼將顯得非常愚蠢。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-14.jpg)上述建議普遍適用,不僅僅對 Go 而言。由於我們編寫的程式的複雜性大大增加,而且我們所反對的限制變得更加多變,程式員應該相應地調整他們的思想。不幸的是,正統的編程思想依然嚴重受到物件導向編程和設計模式至高無上的影響。迄今為止,廣泛傳播的很多想法和最佳實務在很大程度上,幾十年以來,一直沒有受到過挑戰,當前迫切地需要重新思考,尤其是,近年來編程格局和範例已經發生了很大的變化。不改變舊的風格不僅會助長懶惰,而且會讓程式員陷入他們無法承受的虛假的安撫感中。
via: https://medium.com/@copyconstruct/small-functions-considered-harmful-91035d316c29
作者:Cindy Sridharan 譯者:gogeof 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
443 次點擊