函數(shù)是代碼組合的基本單位爬泥,高級編程語言的發(fā)展從結構化到面向對象,再到最近大有要復興之勢的函數(shù)式編程崩瓤,函數(shù)都是組成這座大廈不可或缺的基本組成部分袍啡,它的重要性不言而喻。本文將依據(jù)「clean code」第三章的內容谷遂,大致捋一遍如何寫出優(yōu)雅的函數(shù)葬馋。
第三章講了在寫函數(shù)時應該注意的事情,作者首先拿一個開源的測試工具(Fitnesse)來舉了一個例子肾扰,來說明好的函數(shù)該是什么樣子畴嘶。原則上其實和上一篇中講到的命名的一些原則很相似,就是一個名字要是能夠自解釋的集晚,當然這一章還會講到很多新的東西窗悯,這里拿這個函數(shù)作為一個引子。
//代碼2-1
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite)throws Exception{
boolean isTestPage = pageData.hasAttribute("Test");
if(isTestPage){
WikiPage testPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(test Page, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
}
}
從以上代碼可以看到上一章中提到的一些東西偷拔,不要懼怕你的函數(shù)或者變量名定義的很長蒋院,在編譯器已經長足發(fā)展的今天,對于很長的函數(shù)命名的處理已經不會成為語言或者性能的瓶頸了莲绰;然后整個函數(shù)就像是在敘述做一件事情的步驟欺旧,每一步我們都能看懂這是在干些什么事情,以上這個例子很好的展示了一個「好函數(shù)」應該的樣子蛤签。
我曾經聽過一個Oracle的工程師講到他們的編碼要求辞友,包括一個函數(shù)內部的if不能超過兩個,所有的函數(shù)應該限制在10行以內等震肮,與這個例子的思想都是不謀而合称龙。
下邊開始列舉作者對一個寫好一個函數(shù)應該遵循的原則的描述:
1. 小4辽巍v曜稹!
The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that.
小的函數(shù)不一定好沦偎,但是可以肯定的是太長的函數(shù)一定是在某種程度上很爛的疫向。
作者談到他一直以來的意見是函數(shù)不應該超過一個屏幕能顯示的程度,當然現(xiàn)在的屏幕越來越大豪嚎,那么如果非要使用一個數(shù)字的話搔驼,他認為一行不應該超過150個字符(我認為其實最好還保持在80或120個字符以內),函數(shù)的行數(shù)最好不要超過100行疙渣,如果能少于20行最好不過了匙奴。
作者在這里講述了他和著名的Kent Beck的一段講話來舉例,Kent Beck說一個函數(shù)最多應該不要超過4行妄荔。這個確實有點恐怖泼菌,我覺得能控制在20行以內就已經很厲害了谍肤。函數(shù)要設計的小并不是目的,而是通過寫出來小函數(shù)來達到讓程序的閱讀者可以快速的看懂這段程序哗伯,而且更短的程序往往意味著更少的bug荒揣。接下來會有一些原則,如果你可以理解它們并盡量遵守焊刹,會有效的幫助你寫出小而高質量的代碼系任。
- 代碼塊和縮進
作者在這一節(jié)講了兩個事情,代碼塊(就是if或者else塊)的內容最多不超過一行虐块,這樣這個在塊中的函數(shù)往往可以有一個自解釋的名字俩滥;一個函數(shù)的縮進等級最多不能超過2級。這兩個要求在我看來都是極其嚴苛了贺奠,一般人都做不到霜旧,不過朝著這方面努力總是好的。
2. 只做一件事
一個好的函數(shù)應該只做一件事儡率,這是大家經常見到的說法挂据,那么怎么才能說一個函數(shù)是「只做了一家事」呢,如果一個函數(shù)做了3件事儿普,這而3件事又可以說是另外一件事的3個步驟崎逃,那么這個算不算是「只做一件事」呢?
作者認為這種情況是屬于「只做了一件事」的眉孩,但是這個函數(shù)應該只包含這3個步驟个绍,而不包括3個步驟的具體實現(xiàn),也就是說勺像,如果這個函數(shù)里包含了某一個步驟的具體實現(xiàn)障贸,那么這個函數(shù)就不是「只做一件事」错森。
換言之吟宦,如果一個函數(shù)function1里的幾個語句可以被extract出來成為一個新的函數(shù)function2,那么function1就沒有達到「只做一件事呢」的標準涩维。
- 函數(shù)里的小節(jié)
有些人寫代碼喜歡在一個函數(shù)里使用不同的代碼小節(jié)(比如第一節(jié)定義變量殃姓,第二節(jié)實例化這些變量,第三節(jié)再對這些變量進行一些操作)瓦阐,作者認為這嚴重違反了「只做一件事」原則蜗侈,是非常不好的習慣。
3. 每個函數(shù)只包含同一個層級的抽象
這個原則是比較好理解的睡蟋,比如代碼2-1中的getWikiPage()函數(shù)的內部實現(xiàn)踏幻,和renderPageWithSetupsAndTeardowns()里的幾個函數(shù)調用就不在一個抽象層級上;或者對newPageContent.append()的函數(shù)調用明顯就與其他函數(shù)調用不是在同一抽象層級戳杀。
- 自上而下的閱讀代碼:層層向下
作者覺得好的代碼讀起來應該像是記敘文该面,自上而下的講解完成一件事情的步驟夭苗。換種說法就是閱讀一個程序就像是在閱讀一堆的TO(英文單詞to,為了……)段落:為了做某件事情1隔缀,我們去做了事情2题造,為了做事情2,我們去做了事情3猾瘸。與此同時作者又說覺得這個原則遵循起來相當難界赔,但是這是一個努力的方向,努力去學習這個原則會幫助你寫出來更小的牵触,只包含同一級抽象的函數(shù)淮悼。
4. Switch語句
在coding過程中很難避免要用到switch語句的情況,在這種情況下就很難去保持以上講到的一些規(guī)則揽思,作者的建議是對于Switch語句敛惊,應該將它封裝起來,使用多態(tài)(具體講可能就是定義抽象工廠方法绰更,然后switch可以被放在抽象工廠方法的實現(xiàn)類里瞧挤,同時讓switch對于它的調用者完全透明)為這個函數(shù)的真正使用者提供服務。
5. 使用自解釋(descriptive)的名字
函數(shù)的名字要能夠描述它本身的工作內容儡湾,不要害怕函數(shù)名會變得很長特恬,一個長的自解釋的名字比一個短的不明所以的名字要好得多。
同時在名字的選擇上要前后一致徐钠,這個原則同前一篇講命名中的一些規(guī)則如出一轍癌刽。
6. 函數(shù)參數(shù)
最理想的函數(shù)應該沒有參數(shù)的,其次比較好的是只有一個參數(shù)的尝丐、只有兩個的显拜,包含三個參數(shù)的函數(shù)應該盡量被避免使用,三個以上的參數(shù)的函數(shù)不應該存在爹袁。
含有參數(shù)的函數(shù)明顯已經包含了一個和函數(shù)內容不在同一個層級上的抽象(參數(shù)本身)远荠,還有從測試的觀點看,參數(shù)的存在也提高了寫測試用例的難度失息。
有時候有些參數(shù)還被作為輸出用途譬淳,這種情況應該盡量避免。
「Clean Code」整本書都是基于Java和其他類似的高級語言為基礎的盹兢,但是在一些理念不同的語言中邻梆,含有多個參數(shù)的函數(shù)在理解上是完全沒有問題的,但是它們可能在其他方面(比如編寫測試用例)也會存在各種各樣的問題绎秒。
- 單個參數(shù)
有兩種比較常見的場景適合使用單個參數(shù)的函數(shù)浦妄,一種是「函數(shù)是要對此參數(shù)問一個問題,并得到一個答案」(比如 boolean fileExists("MyFile"),另一種是「此函數(shù)是要對此函數(shù)對此參數(shù)進行一個操作剂娄,把它變成另外一中東西窘问,并將它返回」(InputStream fileOpen("MyFile"))。
此外還有一種場景比較不那么常見宜咒,但是也十分有用的單個參數(shù)函數(shù)形式惠赫,event。這種情況下函數(shù)接受一個參數(shù)event故黑,但是沒有返回值儿咱,函數(shù)會根據(jù)這個event對象來進行一些其他操作。
使用這些形式時也同時要使用一個合適的名字來清晰的描述函數(shù)的用途场晶,從而讓代碼閱讀者可以清晰快速地了解函數(shù)的目的混埠。
Flag參數(shù)
Flag參數(shù)是丑陋的,不應該被使用诗轻;往往一個含有Flag參數(shù)的函數(shù)可以被分解或簡單的使用if else代碼塊就可以達到同樣的效果钳宪。兩個參數(shù)
作者認為兩個參數(shù)的函數(shù)是難以理解的,唯一可能比較合適的場景是扳炬,這兩個參數(shù)本身就是「一個單獨對象的兩個有序的組件」吏颖,比如Point(x, y),x軸坐標和y軸坐標共同組成了平面坐標系中的一個點恨樟。
但是半醉,作者認為兩個參數(shù)的函數(shù)并不是邪惡了,所有人都不可避免的在實際編程中使用它們劝术,但是一定要了解它們是會帶來一些不良后果的缩多,最好是可以把這些函數(shù)都轉化成單個參數(shù)的函數(shù)。三個參數(shù)
三個參數(shù)的函數(shù)非常難以理解甚至常常會被誤解养晋,所以在使用前最好三思再三思衬吆。參數(shù)對象
當一個函數(shù)需要2個或者3個參數(shù)是,往往可以將他們全部或者一部分封裝成一個對象绳泉,從而達到減少參數(shù)數(shù)量逊抡,使函數(shù)更加易于理解和維護的目的。-
參數(shù)列表
有時候一個函數(shù)會接受一個變長的參數(shù)列表圈纺,這種情況下其實它們可以看做一個List結果的參數(shù)對象秦忿,所以可以被看做一個單個參數(shù)的函數(shù)麦射,作者是比較推薦這種方式的蛾娶。public String format(String format, Object...args)
動詞和關鍵詞
使用一個好的函數(shù)名可以清晰的解釋函數(shù)和參數(shù)的目的。對于單個參數(shù)的函數(shù)潜秋,函數(shù)和參數(shù)應該是一個「動詞 + 名詞」的組合蛔琅。
還有一種「關鍵詞」的模式來作為函數(shù)的名字,比如使用assertEquals(expected, actural),而不是assertExpectedEqualsActual(expected, actural)峻呛,這樣就不需要讀者必須知道參數(shù)的順序罗售,從而降低了閱讀此代碼的難度辜窑。
7. 不要有副作用
函數(shù)的副作用就像謊言一樣,一個函數(shù)聲稱它要做一件事寨躁,但是同時它又做了另外一件「隱藏的」事穆碎,有時候它會修改自己的類中的屬性,有時候它會修改傳進來的參數(shù)或者其他全局變量职恳,不管哪種情況所禀,這都是不好的。
- 輸出參數(shù)
在「面向對象編程」出現(xiàn)之前(比如C語言)放钦,有時候必須使用一個參數(shù)(通常是一個指針)來作為函數(shù)的輸出色徘,但是在「OOP」中,「this」指針往往隱喻了它是用來作為輸出指針的作用操禀,那么輸出參數(shù)應該盡可能的避免使用褂策,如果有這樣的需求,修改「this」中的屬性往往是更好的選擇颓屑。
8. 執(zhí)行和檢索分離
一個函數(shù)要不執(zhí)行了某個行為(比如改變了一個對象的狀態(tài))斤寂,要不回答了某個問題(比如返回某個對象的某些信息),但是不應該同時做這樣兩件事揪惦。
9. 使用Exception扬蕊,不要使用返回錯誤碼
返回錯誤碼輕微地違反了上一個規(guī)則,作者建議不要使用返回錯誤碼而使用拋出Exception的方法丹擎。這樣的方法往往會時代碼更短而清晰易讀尾抑。
提取try/catch代碼塊
作者建議在使用try代碼塊時要將try中的代碼提取,使得整個try/catch代碼塊是一個完全的錯誤處理代碼塊蒂培,而不包含具體的其他操作邏輯再愈,這就符合了「只做一件事」的原則。-
錯誤處理是「一件事」
作者建議做錯誤處理的函數(shù)护戳,應該只做錯誤處理這「一件事」翎冲,也就是說一個包含try關鍵詞的錯誤處理函數(shù)應該是以try這個單詞為開始的。如代碼2-2所示:代碼2-2 public void delete(Page page){ try{ deletePageAndAllReferences(page). }catch(Exception e){ logError(e); } }
依賴磁鐵
返回錯誤碼的方法還有一個問題是媳荒,往往在這種場景下抗悍,所有的需要錯誤處理的地方都會需要依賴這個類或者文件,這樣的被廣泛依賴的類叫做「依賴磁鐵」
依賴磁鐵在普通的日常開發(fā)中很難避免钳枕,而且我覺得這也不是一個需要強力避免的原則缴渊。
10. 不要重復自己(Don't repeat yourself)
我們在寫代碼時往往會將同樣一個算法、或者一段處理邏輯鱼炒、甚至一段相同的代碼重復的出現(xiàn)在多個地方衔沼,甚至是同一個源文件的不同地方。這往往是很多代碼質量問題的源頭,也有很多編程原則和最佳實踐都是為了控制或者消滅重復而產生的指蚁。
- 結構化編程
作者認為如果能保證保持函數(shù)「很小」菩佑,那么很多問題就可以解決了,那么結構化編程的一些規(guī)則(比如一個函數(shù)只有一個輸入和一個輸出)則不需要被遵守了凝化。
11. 你如何才能寫出這樣的函數(shù)
程序員寫代碼跟其他類型的寫作一樣稍坯,是一個不斷改善的過程。作者認為寫出好的函數(shù)大致是這樣幾個步驟
- 先直接把想法寫成代碼搓劫,它們可能是很長而且復雜的劣光,可能違反了以上很多規(guī)則,同時也會有一套單元測試來cover所有的代碼用來做回歸測試糟把,做為將來對代碼重構的基礎
- 然后開始對代碼進行修改绢涡,分離函數(shù),修改名稱遣疯,消滅重復雄可,同時要保持測試用例完全通過。
- 最后使用以上所有規(guī)則來對函數(shù)進行地毯式的最后修改缠犀。
12. 總結
如果你遵循以上所有的規(guī)則数苫,你的函數(shù)會變得體量短小、良好命名辨液、并且具有良好的組織結構虐急。但是永遠不要忘記這不是目的而是手段,你的最終目的是讓整個系統(tǒng)更加完美滔迈。