最近讀到了一篇很好的關於編程思考的文章,思考之後整理一下,尤其是裏面的一些代碼片段,很有代表性,希望以後回望時仍然有收穫。
原文地址:http://kb.cnblogs.com/page/549080/
編程是創造性的工作,需要靈感和汗水,需要不斷思考和實踐,同時還需要有人指點迷津,以使自己以最優的方式成長,兼具速度和質量。
反覆推敲代碼
提高編程水平最有效的辦法是什麼?是反反覆覆地修改和推敲代碼。
有位文豪說得好:“看一個作家的水平,不是看他發表了多少文字,而要看他的廢紙簍裏扔掉了多少。”同樣的理論適用於編程。好的程序員,他們刪掉的代碼,比留下來的還要多很多。去糟粕,留精華,這是普遍規律。
寫優雅的代碼
優雅的代碼整整齊齊,邏輯清晰,無論是功能分類,還是流程細節,都讓人覺得從容,優雅。
程序所做的幾乎一切事情,都是信息的傳遞和分支。類比電路,電流經過導線,分流或者匯合。如果你是這樣思考的,你的代碼裏就會比較少出現只有一個分支的 if 語句,它看起來就會像這個樣子:if (...) { if (...) { ... } else { ... } } else if (...) { ... } else { ... }
注意到了嗎?在上面的代碼裏面,if 語句幾乎總是有兩個分支。它們有可能嵌套,有多層的縮進,而且 else 分支裏面有可能出現少量重複的代碼。然而這樣的結構,邏輯卻非常嚴密和清晰。寫模塊化的代碼
模塊化的代碼,不是簡單將功能文件放入不同文件和目錄,也不是強行將不同功能分成不同函數。一個模塊應該像一個電路芯片,有明確的輸入和輸出。實際上一種很好的模塊化方法早已經存在,它的名字叫做“函數”。每一個函數都有明確的輸入(參數)和輸出(返回值),同一個文件裏可以包含多個函數,所以其實根本不需要把代碼分開在多個文件或者目錄裏面,同樣可以完成代碼的模塊化。甚至把代碼全都寫在同一個文件裏,卻仍然是非常模塊化的代碼。
想要做到代碼模塊化,以下幾點很關鍵:
1.避免函數太長。
40-50行即可,一頁屏幕或人眼觀察能力基本就是4、50行,過長的代碼不僅不易讀而且容易造成邏輯混亂。
2.製作小的工具函數。
一些常用的功能會在代碼中反覆使用(如輸出信息到UI、時間統計等等),提煉成小的工具函數有利於效率和邏輯性的提升。
3.每個函數只做一件簡單的事。
有些人喜歡製造一些“通用”的函數,既可以做這個又可以做那個,它的內部依據某些變量和條件,來“選擇”這個函數所要做的事情。比如,你也許寫出這樣的函數(注意,很多人願意這樣寫):
void foo () { if (getOS () .equals ("MacOS")) { a (); } else { b (); } c (); if (getOS () .equals ("MacOS")) { d (); } else { e (); } }
這種“複用”其實是有害的。如果一個函數可能做兩種事情,它們之間共同點少於它們的不同點,那你最好就寫兩個不同的函數,否則這個函數的邏輯就不會很清晰,容易出現錯誤。其實,上面這個函數可以改寫成兩個函數:<span style="font-weight: normal; font-size: 14px; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">寫這個函數的人,根據系統是否爲“MacOS”來做不同的事情。你可以看出這個函數裏,其實只有c()是兩種系統共有的,而其它的a(), b(), d(),e()都屬於不同的分支。</span>
void fooMacOS () { a (); c (); d (); }
和
void fooOther () { b (); c (); e (); }
如果你發現兩件事情大部分內容相同,只有少數不同,多半時候你可以把相同的部分提取出去,做成一個輔助函數。比如,如果你有個函數是這樣:
void foo () { a (); b () c (); if (getOS () .equals ("MacOS")) { d (); } else { e (); } }
其中a(),b(),c()都是一樣的,只有d()和e()根據系統有所不同。那麼你可以把a(),b(),c()提取出去:
void preFoo () { a (); b () c (); }
然後製造兩個函數:<span style="font-size: 14px; font-weight: normal; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"></span><pre name="code" class="cpp">void fooMacOS () { preFoo (); d (); }
<span style="font-size: 14px; font-weight: normal; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">和</span>
<span style="font-size: 14px; font-weight: normal; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"></span><pre name="code" class="cpp">void fooOther () { preFoo (); e (); }
<span style="font-weight: normal; font-size: 14px; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">這樣一來,我們既共享了代碼,又做到了每個函數只做一件簡單的事情。這樣的代碼,邏輯就更加清晰。</span>
4.避免使用全局變量和類成員(class member)來傳遞信息,儘量使用局部變量和參數。有些人寫代碼,經常用類成員來傳遞信息,就像這樣(本人之前一直這麼用):
class A { String x; void findX () { ... x = ...; } void foo () { findX (); ... print (x); } }
首先,他使用findX(),把一個值寫入成員x。然後,使用x的值。這樣,x就變成了findX和print之間的數據通道。由於x屬於class A,這樣程序就失去了模塊化的結構。由於這兩個函數依賴於成員x,它們不再有明確的輸入和輸出,而是依賴全局的數據。findX和foo不再能夠離開class A而存在,而且由於類成員還有可能被其他代碼改變,代碼變得難以理解,難以確保正確性。
如果你使用局部變量而不是類成員來傳遞信息,那麼這兩個函數就不需要依賴於某一個 class,而且更加容易理解,不易出錯:
String findX () { ... x = ...; return x; } void foo () { int x = findX (); print (x); }
寫可讀的代碼
說到可讀的代碼,很多人第一反應就是註釋,有很多編程規範要求註釋量要達到代碼總量的30%甚至更高,當然這個問題衆說紛紜,但個人認爲,良好的代碼風格比添加註釋更能說明一段代碼的功能和含義,過多的註釋不僅破壞代碼完整性,而且一旦代碼修改,很多註釋會失效,註釋的添加和修改成爲很多程序員不願觸碰之殤。
註釋常用在以下典型位置:1.說明主要流程時
2.在違反常規思維的設計時
3.在值得留意或預留功能時
同時,以下方法可以幫助你減少註釋量的同時維持程序可讀性:
1.使用有意義的函數和變量名字。
如果你的函數和變量的名字,能夠切實的描述它們的邏輯,那麼你就不需要寫註釋來解釋。比如:
// put elephant1 into fridge2 put (elephant1, fridge2);
2.局部變量應該儘量接近使用它的地方。有些人喜歡在函數最開頭定義很多局部變量,然後在下面很遠的地方使用它,其實可以挪到接近使用它的地方:就像這個樣子:
void foo () { ... ... int index = ...; bar (index); ... }
這樣讀者看到bar (index),不需要向上看很遠就能發現index是如何算出來的。而且這種短距離,可以加強讀者對於這裏的“計算順序”的理解。否則如果 index 在頂上,讀者可能會懷疑,它其實保存了某種會變化的數據,或者它後來又被修改過。如果 index 放在下面,讀者就清楚的知道,index 並不是保存了什麼可變的值,而且它算出來之後就沒變過。
如果你看透了局部變量的本質——它們就是電路里的導線,那你就能更好的理解近距離的好處。變量定義離用的地方越近,導線的長度就越短。你不需要摸着一根導線,繞來繞去找很遠,就能發現接收它的端口,這樣的電路就更容易理解。
3.局部變量名字應該簡短。
這貌似跟第一點相沖突,簡短的變量名怎麼可能有意義呢?注意我這裏說的是局部變量,因爲它們處於局部,再加上第 2 點已經把它放到離使用位置儘量近的地方,所以根據上下文你就會容易知道它的意思:boolean success = deleteFile ("foo.txt"); if (success) { ... } else { ... }
4.不要重用局部變量。以下是一個重用的反例:
String msg; if (...) { msg = "succeed"; log.info (msg); } else { msg = "failed"; log.info (msg); }
從讀者心裏來講,看見msg被多次賦值,會思考msg有沒有在其他地方賦值,這裏用它準備嗎等等之類的懷疑。簡單改成這樣會好得多:if (...) { String msg = "succeed"; log.info (msg); } else { String msg = "failed"; log.info (msg); }
5.把複雜的邏輯提取出去,做成“幫助函數”。有些人寫的函數很長,以至於看不清楚裏面的語句在幹什麼,所以他們誤以爲需要寫註釋。如果你仔細觀察這些代碼,就會發現不清晰的那片代碼,往往可以被提取出去,做成一個函數,然後在原來的地方調用。由於函數有一個名字,這樣你就可以使用有意義的函數名來代替註釋。舉一個例子:
... // put elephant1 into fridge2 openDoor (fridge2); if (elephant1.alive ()) { ... } else { ... } closeDoor (fridge2); ...
如果你把這片代碼提出去定義成一個函數:void put (Elephant elephant, Fridge fridge) { openDoor (fridge); if (elephant.alive ()) { ... } else { ... } closeDoor (fridge); }
這樣原來的代碼就可以改成:... put (elephant1, fridge2); ...
更加清晰,註釋也沒必要了。
6.把複雜的表達式提取出去,做成中間變量。Crust crust = crust (salt (), butter ()); Topping topping = topping (onion (), tomato (), sausage ()); Pizza pizza = makePizza (crust, topping);
7.在合理的地方換行。if (someLongCondition1() && someLongCondition2() && someLongCondition3() && someLongCondition4()) { ... }
寫簡單的代碼
簡單並不代表省略,以下幾條建議會幫助你避免因爲追求簡單而犯錯:
1.永遠不要省略花括號{}
2.合理使用括號(),不盲目依賴操作符優先級
3.避免使用continue和break
第3條很多人會有疑問,我也思考了一陣,個人認爲這是個仁者見仁智者見智的問題,從原文的角度考慮,continue和break是破壞程序順序執行的額外加入的強邏輯手段,可以考慮這樣改寫:
1)如果出現了 continue,你往往只需要把 continue 的條件反向,就可以消除 continue。
2)如果出現了 break,你往往可以把 break 的條件,合併到循環頭部的終止條件裏,從而去掉 break。(這個有點牽強)
3)有時候你可以把 break 替換成 return,從而去掉 break。
4)如果以上都失敗了,你也許可以把循環裏面複雜的部分提取出來,做成函數調用,之後 continue 或者 break 就可以去掉了。對應的舉例我就省略了,有興趣的可以看原文。
文中還提及如何處理錯誤、如何處理NULL指針等等,後續我另寫文章來總結。