函數(shù)是代碼組合的基本單位,高級(jí)編程語(yǔ)言的發(fā)展從結(jié)構(gòu)化到面向?qū)ο?,再到最近大有要?fù)興之勢(shì)的函數(shù)式編程,函數(shù)都是組成這座大廈不可或缺的基本組成部分,它的重要性不言而喻。本文將依據(jù)「clean code」第三章的內(nèi)容,大致捋一遍如何寫(xiě)出優(yōu)雅的函數(shù)。
第三章講了在寫(xiě)函數(shù)時(shí)應(yīng)該注意的事情,作者首先拿一個(gè)開(kāi)源的測(cè)試工具(Fitnesse)來(lái)舉了一個(gè)例子,來(lái)說(shuō)明好的函數(shù)該是什么樣子。原則上其實(shí)和上一篇中講到的命名的一些原則很相似,就是一個(gè)名字要是能夠自解釋的,當(dāng)然這一章還會(huì)講到很多新的東西,這里拿這個(gè)函數(shù)作為一個(gè)引子。
//代碼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ù)或者變量名定義的很長(zhǎng),在編譯器已經(jīng)長(zhǎng)足發(fā)展的今天,對(duì)于很長(zhǎng)的函數(shù)命名的處理已經(jīng)不會(huì)成為語(yǔ)言或者性能的瓶頸了;然后整個(gè)函數(shù)就像是在敘述做一件事情的步驟,每一步我們都能看懂這是在干些什么事情,以上這個(gè)例子很好的展示了一個(gè)「好函數(shù)」應(yīng)該的樣子。
我曾經(jīng)聽(tīng)過(guò)一個(gè)Oracle的工程師講到他們的編碼要求,包括一個(gè)函數(shù)內(nèi)部的if不能超過(guò)兩個(gè),所有的函數(shù)應(yīng)該限制在10行以內(nèi)等,與這個(gè)例子的思想都是不謀而合。
下邊開(kāi)始列舉作者對(duì)一個(gè)寫(xiě)好一個(gè)函數(shù)應(yīng)該遵循的原則的描述:
1. ?。。?!
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ù)不一定好,但是可以肯定的是太長(zhǎng)的函數(shù)一定是在某種程度上很爛的。
作者談到他一直以來(lái)的意見(jiàn)是函數(shù)不應(yīng)該超過(guò)一個(gè)屏幕能顯示的程度,當(dāng)然現(xiàn)在的屏幕越來(lái)越大,那么如果非要使用一個(gè)數(shù)字的話,他認(rèn)為一行不應(yīng)該超過(guò)150個(gè)字符(我認(rèn)為其實(shí)最好還保持在80或120個(gè)字符以內(nèi)),函數(shù)的行數(shù)最好不要超過(guò)100行,如果能少于20行最好不過(guò)了。
作者在這里講述了他和著名的Kent Beck的一段講話來(lái)舉例,Kent Beck說(shuō)一個(gè)函數(shù)最多應(yīng)該不要超過(guò)4行。這個(gè)確實(shí)有點(diǎn)恐怖,我覺(jué)得能控制在20行以內(nèi)就已經(jīng)很厲害了。函數(shù)要設(shè)計(jì)的小并不是目的,而是通過(guò)寫(xiě)出來(lái)小函數(shù)來(lái)達(dá)到讓程序的閱讀者可以快速的看懂這段程序,而且更短的程序往往意味著更少的bug。接下來(lái)會(huì)有一些原則,如果你可以理解它們并盡量遵守,會(huì)有效的幫助你寫(xiě)出小而高質(zhì)量的代碼。
- 代碼塊和縮進(jìn)
作者在這一節(jié)講了兩個(gè)事情,代碼塊(就是if或者else塊)的內(nèi)容最多不超過(guò)一行,這樣這個(gè)在塊中的函數(shù)往往可以有一個(gè)自解釋的名字;一個(gè)函數(shù)的縮進(jìn)等級(jí)最多不能超過(guò)2級(jí)。這兩個(gè)要求在我看來(lái)都是極其嚴(yán)苛了,一般人都做不到,不過(guò)朝著這方面努力總是好的。
2. 只做一件事
一個(gè)好的函數(shù)應(yīng)該只做一件事,這是大家經(jīng)常見(jiàn)到的說(shuō)法,那么怎么才能說(shuō)一個(gè)函數(shù)是「只做了一家事」呢,如果一個(gè)函數(shù)做了3件事,這而3件事又可以說(shuō)是另外一件事的3個(gè)步驟,那么這個(gè)算不算是「只做一件事」呢?
作者認(rèn)為這種情況是屬于「只做了一件事」的,但是這個(gè)函數(shù)應(yīng)該只包含這3個(gè)步驟,而不包括3個(gè)步驟的具體實(shí)現(xiàn),也就是說(shuō),如果這個(gè)函數(shù)里包含了某一個(gè)步驟的具體實(shí)現(xiàn),那么這個(gè)函數(shù)就不是「只做一件事」。
換言之,如果一個(gè)函數(shù)function1里的幾個(gè)語(yǔ)句可以被extract出來(lái)成為一個(gè)新的函數(shù)function2,那么function1就沒(méi)有達(dá)到「只做一件事呢」的標(biāo)準(zhǔn)。
- 函數(shù)里的小節(jié)
有些人寫(xiě)代碼喜歡在一個(gè)函數(shù)里使用不同的代碼小節(jié)(比如第一節(jié)定義變量,第二節(jié)實(shí)例化這些變量,第三節(jié)再對(duì)這些變量進(jìn)行一些操作),作者認(rèn)為這嚴(yán)重違反了「只做一件事」原則,是非常不好的習(xí)慣。
3. 每個(gè)函數(shù)只包含同一個(gè)層級(jí)的抽象
這個(gè)原則是比較好理解的,比如代碼2-1中的getWikiPage()函數(shù)的內(nèi)部實(shí)現(xiàn),和renderPageWithSetupsAndTeardowns()里的幾個(gè)函數(shù)調(diào)用就不在一個(gè)抽象層級(jí)上;或者對(duì)newPageContent.append()的函數(shù)調(diào)用明顯就與其他函數(shù)調(diào)用不是在同一抽象層級(jí)。
- 自上而下的閱讀代碼:層層向下
作者覺(jué)得好的代碼讀起來(lái)應(yīng)該像是記敘文,自上而下的講解完成一件事情的步驟。換種說(shuō)法就是閱讀一個(gè)程序就像是在閱讀一堆的TO(英文單詞to,為了……)段落:為了做某件事情1,我們?nèi)プ隽耸虑?,為了做事情2,我們?nèi)プ隽耸虑?。與此同時(shí)作者又說(shuō)覺(jué)得這個(gè)原則遵循起來(lái)相當(dāng)難,但是這是一個(gè)努力的方向,努力去學(xué)習(xí)這個(gè)原則會(huì)幫助你寫(xiě)出來(lái)更小的,只包含同一級(jí)抽象的函數(shù)。
4. Switch語(yǔ)句
在coding過(guò)程中很難避免要用到switch語(yǔ)句的情況,在這種情況下就很難去保持以上講到的一些規(guī)則,作者的建議是對(duì)于Switch語(yǔ)句,應(yīng)該將它封裝起來(lái),使用多態(tài)(具體講可能就是定義抽象工廠方法,然后switch可以被放在抽象工廠方法的實(shí)現(xiàn)類(lèi)里,同時(shí)讓switch對(duì)于它的調(diào)用者完全透明)為這個(gè)函數(shù)的真正使用者提供服務(wù)。
5. 使用自解釋(descriptive)的名字
函數(shù)的名字要能夠描述它本身的工作內(nèi)容,不要害怕函數(shù)名會(huì)變得很長(zhǎng),一個(gè)長(zhǎng)的自解釋的名字比一個(gè)短的不明所以的名字要好得多。
同時(shí)在名字的選擇上要前后一致,這個(gè)原則同前一篇講命名中的一些規(guī)則如出一轍。
6. 函數(shù)參數(shù)
最理想的函數(shù)應(yīng)該沒(méi)有參數(shù)的,其次比較好的是只有一個(gè)參數(shù)的、只有兩個(gè)的,包含三個(gè)參數(shù)的函數(shù)應(yīng)該盡量被避免使用,三個(gè)以上的參數(shù)的函數(shù)不應(yīng)該存在。
含有參數(shù)的函數(shù)明顯已經(jīng)包含了一個(gè)和函數(shù)內(nèi)容不在同一個(gè)層級(jí)上的抽象(參數(shù)本身),還有從測(cè)試的觀點(diǎn)看,參數(shù)的存在也提高了寫(xiě)測(cè)試用例的難度。
有時(shí)候有些參數(shù)還被作為輸出用途,這種情況應(yīng)該盡量避免。
「Clean Code」整本書(shū)都是基于Java和其他類(lèi)似的高級(jí)語(yǔ)言為基礎(chǔ)的,但是在一些理念不同的語(yǔ)言中,含有多個(gè)參數(shù)的函數(shù)在理解上是完全沒(méi)有問(wèn)題的,但是它們可能在其他方面(比如編寫(xiě)測(cè)試用例)也會(huì)存在各種各樣的問(wèn)題。
- 單個(gè)參數(shù)
有兩種比較常見(jiàn)的場(chǎng)景適合使用單個(gè)參數(shù)的函數(shù),一種是「函數(shù)是要對(duì)此參數(shù)問(wèn)一個(gè)問(wèn)題,并得到一個(gè)答案」(比如 boolean fileExists("MyFile"),另一種是「此函數(shù)是要對(duì)此函數(shù)對(duì)此參數(shù)進(jìn)行一個(gè)操作,把它變成另外一中東西,并將它返回」(InputStream fileOpen("MyFile"))。
此外還有一種場(chǎng)景比較不那么常見(jiàn),但是也十分有用的單個(gè)參數(shù)函數(shù)形式,event。這種情況下函數(shù)接受一個(gè)參數(shù)event,但是沒(méi)有返回值,函數(shù)會(huì)根據(jù)這個(gè)event對(duì)象來(lái)進(jìn)行一些其他操作。
使用這些形式時(shí)也同時(shí)要使用一個(gè)合適的名字來(lái)清晰的描述函數(shù)的用途,從而讓代碼閱讀者可以清晰快速地了解函數(shù)的目的。
Flag參數(shù)
Flag參數(shù)是丑陋的,不應(yīng)該被使用;往往一個(gè)含有Flag參數(shù)的函數(shù)可以被分解或簡(jiǎn)單的使用if else代碼塊就可以達(dá)到同樣的效果。兩個(gè)參數(shù)
作者認(rèn)為兩個(gè)參數(shù)的函數(shù)是難以理解的,唯一可能比較合適的場(chǎng)景是,這兩個(gè)參數(shù)本身就是「一個(gè)單獨(dú)對(duì)象的兩個(gè)有序的組件」,比如Point(x, y),x軸坐標(biāo)和y軸坐標(biāo)共同組成了平面坐標(biāo)系中的一個(gè)點(diǎn)。
但是,作者認(rèn)為兩個(gè)參數(shù)的函數(shù)并不是邪惡了,所有人都不可避免的在實(shí)際編程中使用它們,但是一定要了解它們是會(huì)帶來(lái)一些不良后果的,最好是可以把這些函數(shù)都轉(zhuǎn)化成單個(gè)參數(shù)的函數(shù)。三個(gè)參數(shù)
三個(gè)參數(shù)的函數(shù)非常難以理解甚至常常會(huì)被誤解,所以在使用前最好三思再三思。參數(shù)對(duì)象
當(dāng)一個(gè)函數(shù)需要2個(gè)或者3個(gè)參數(shù)是,往往可以將他們?nèi)炕蛘咭徊糠址庋b成一個(gè)對(duì)象,從而達(dá)到減少參數(shù)數(shù)量,使函數(shù)更加易于理解和維護(hù)的目的。-
參數(shù)列表
有時(shí)候一個(gè)函數(shù)會(huì)接受一個(gè)變長(zhǎng)的參數(shù)列表,這種情況下其實(shí)它們可以看做一個(gè)List結(jié)果的參數(shù)對(duì)象,所以可以被看做一個(gè)單個(gè)參數(shù)的函數(shù),作者是比較推薦這種方式的。public String format(String format, Object...args) 動(dòng)詞和關(guān)鍵詞
使用一個(gè)好的函數(shù)名可以清晰的解釋函數(shù)和參數(shù)的目的。對(duì)于單個(gè)參數(shù)的函數(shù),函數(shù)和參數(shù)應(yīng)該是一個(gè)「動(dòng)詞 + 名詞」的組合。
還有一種「關(guān)鍵詞」的模式來(lái)作為函數(shù)的名字,比如使用assertEquals(expected, actural),而不是assertExpectedEqualsActual(expected, actural),這樣就不需要讀者必須知道參數(shù)的順序,從而降低了閱讀此代碼的難度。
7. 不要有副作用
函數(shù)的副作用就像謊言一樣,一個(gè)函數(shù)聲稱它要做一件事,但是同時(shí)它又做了另外一件「隱藏的」事,有時(shí)候它會(huì)修改自己的類(lèi)中的屬性,有時(shí)候它會(huì)修改傳進(jìn)來(lái)的參數(shù)或者其他全局變量,不管哪種情況,這都是不好的。
- 輸出參數(shù)
在「面向?qū)ο缶幊獭钩霈F(xiàn)之前(比如C語(yǔ)言),有時(shí)候必須使用一個(gè)參數(shù)(通常是一個(gè)指針)來(lái)作為函數(shù)的輸出,但是在「OOP」中,「this」指針往往隱喻了它是用來(lái)作為輸出指針的作用,那么輸出參數(shù)應(yīng)該盡可能的避免使用,如果有這樣的需求,修改「this」中的屬性往往是更好的選擇。
8. 執(zhí)行和檢索分離
一個(gè)函數(shù)要不執(zhí)行了某個(gè)行為(比如改變了一個(gè)對(duì)象的狀態(tài)),要不回答了某個(gè)問(wèn)題(比如返回某個(gè)對(duì)象的某些信息),但是不應(yīng)該同時(shí)做這樣兩件事。
9. 使用Exception,不要使用返回錯(cuò)誤碼
返回錯(cuò)誤碼輕微地違反了上一個(gè)規(guī)則,作者建議不要使用返回錯(cuò)誤碼而使用拋出Exception的方法。這樣的方法往往會(huì)時(shí)代碼更短而清晰易讀。
提取try/catch代碼塊
作者建議在使用try代碼塊時(shí)要將try中的代碼提取,使得整個(gè)try/catch代碼塊是一個(gè)完全的錯(cuò)誤處理代碼塊,而不包含具體的其他操作邏輯,這就符合了「只做一件事」的原則。-
錯(cuò)誤處理是「一件事」
作者建議做錯(cuò)誤處理的函數(shù),應(yīng)該只做錯(cuò)誤處理這「一件事」,也就是說(shuō)一個(gè)包含try關(guān)鍵詞的錯(cuò)誤處理函數(shù)應(yīng)該是以try這個(gè)單詞為開(kāi)始的。如代碼2-2所示:代碼2-2 public void delete(Page page){ try{ deletePageAndAllReferences(page). }catch(Exception e){ logError(e); } } 依賴磁鐵
返回錯(cuò)誤碼的方法還有一個(gè)問(wèn)題是,往往在這種場(chǎng)景下,所有的需要錯(cuò)誤處理的地方都會(huì)需要依賴這個(gè)類(lèi)或者文件,這樣的被廣泛依賴的類(lèi)叫做「依賴磁鐵」
依賴磁鐵在普通的日常開(kāi)發(fā)中很難避免,而且我覺(jué)得這也不是一個(gè)需要強(qiáng)力避免的原則。
10. 不要重復(fù)自己(Don't repeat yourself)
我們?cè)趯?xiě)代碼時(shí)往往會(huì)將同樣一個(gè)算法、或者一段處理邏輯、甚至一段相同的代碼重復(fù)的出現(xiàn)在多個(gè)地方,甚至是同一個(gè)源文件的不同地方。這往往是很多代碼質(zhì)量問(wèn)題的源頭,也有很多編程原則和最佳實(shí)踐都是為了控制或者消滅重復(fù)而產(chǎn)生的。
- 結(jié)構(gòu)化編程
作者認(rèn)為如果能保證保持函數(shù)「很小」,那么很多問(wèn)題就可以解決了,那么結(jié)構(gòu)化編程的一些規(guī)則(比如一個(gè)函數(shù)只有一個(gè)輸入和一個(gè)輸出)則不需要被遵守了。
11. 你如何才能寫(xiě)出這樣的函數(shù)
程序員寫(xiě)代碼跟其他類(lèi)型的寫(xiě)作一樣,是一個(gè)不斷改善的過(guò)程。作者認(rèn)為寫(xiě)出好的函數(shù)大致是這樣幾個(gè)步驟
- 先直接把想法寫(xiě)成代碼,它們可能是很長(zhǎng)而且復(fù)雜的,可能違反了以上很多規(guī)則,同時(shí)也會(huì)有一套單元測(cè)試來(lái)cover所有的代碼用來(lái)做回歸測(cè)試,做為將來(lái)對(duì)代碼重構(gòu)的基礎(chǔ)
- 然后開(kāi)始對(duì)代碼進(jìn)行修改,分離函數(shù),修改名稱,消滅重復(fù),同時(shí)要保持測(cè)試用例完全通過(guò)。
- 最后使用以上所有規(guī)則來(lái)對(duì)函數(shù)進(jìn)行地毯式的最后修改。
12. 總結(jié)
如果你遵循以上所有的規(guī)則,你的函數(shù)會(huì)變得體量短小、良好命名、并且具有良好的組織結(jié)構(gòu)。但是永遠(yuǎn)不要忘記這不是目的而是手段,你的最終目的是讓整個(gè)系統(tǒng)更加完美。