這本書提出一種觀念:代碼質量與其整潔度成正比。干凈的代碼,既在質量上較為可靠,也為后期維護、升級奠定了良好基礎。(作者認為書可以有另一個名字:《如何在意代碼》)
讀這本書,促使我思考代碼中何謂正確,何謂錯誤。更重要的是,它還可以促使自己重新評估自己的專業價值觀,以及對自己技藝的承諾。
1、整潔編程
- 混亂風險:制造混亂無助於趕上期限。混亂只會立刻拖慢你,叫你錯過期限,趕上期限的唯一方法——做得快的唯一方法——就是始終盡可能的保持代碼整潔。
- 代碼感:寫整潔代碼,需要遵循大量的小技巧,貫徹刻苦習得的“整潔感”。這種“代碼感”就是關鍵所在。缺乏“代碼感”的程序員,看混亂是混亂,無處着手。有“代碼感”的程序員能從混亂中看出其他的可能與變化。“代碼感”幫助程序員選出最好的方案,並指導程序員制訂修改行動計划,按圖索驥。
- “整潔的代碼只做好一件事”——Bjarne Stroustrup(C++語言發明者)。軟件設計的許多原則最終都會歸結為這句警語。糟糕的代碼想做太多事,它意圖混亂、目的含混。整潔的代碼力求集中。每個函數、每個類和每個模塊都全神貫注於一事,完全不受四周細節的干擾和污染。
整潔的代碼應可由作者之外的開發者閱讀和增補。它應當有單元測試和驗收測試。它使用有意義的命名。它只提供一種而非多種做一件事的途徑。它只有盡量少的依賴關系,而且要明確地定義和提供清晰、盡量少的API。代碼應通過其字面表達含義,因為不同的語言導致並非所有必需信息均可通過代碼自身清晰表達。——Dave Thomes(OTI公司創始人,Eclipse戰略教父)
- 貝克的簡單代碼規則:消除重復並提高表達力,提早構建簡單抽象
- 能通過所有測試
- 沒有重復代碼
- 體現系統中的全部設計理念
- 包括盡量少的實體,比如類、方法、函數等
2、有意義的命名
- 名副其實
- 避免誤導
- 做有意義的區分
- 使用讀的出來的名稱
- 使用可搜索的名稱
- 單字母名稱僅用於短方法中的本地變量。名稱長短應與其作用域大小相對應。若變量或常量可能在代碼中多處使用,則應賦其以便於搜索的名稱。
- 避免使用編碼
- 不必用m_前綴來標明成員變量。應當把類和函數做的足夠小,消除對成員前綴的需要。(人們會很快學會無視前綴或后綴,只看到名稱中有意義的部分。代碼讀的越多,眼中就越沒有前綴。最終,前綴變作了不入法眼的廢料,變作了舊代碼的標志物)
- 避免思維映射
- 單字母變量名就是個問題。在多數除循環計數器之外的其他情況下,單字母名稱不是個好選擇,讀者必須在腦中將它映射為真實概念。(僅僅是因為有了a和b,就要取名為c,實在並非像樣的理由。)
- 聰明程序員和專業程序員之間的區別在於:專業程序員了解,明確是王道。專業程序員善用其能,編寫其他人能理解的代碼。
- 類名
- 類名和對象名應該是名詞或名詞短語,如Customer、WikiPage、Account和AddressParser
- 避免使用Mannager、Processor、Data、或Info這樣的類名。
- 方法名
- 方法名應當是動詞或動詞短語,如postPayment、deletePage或save
- 屬性訪問器、修改器和斷言應該根據其值命名,並依Javabean標准加上get、set和is前綴。
string name = employee.getname(); customer.setName("mike"); if (paycheck.isPosted())...
- 別扮可愛
- 言到意到,意到言到。
- 每個概念對應一個詞
- 給每個抽象概念選一個詞,並且一以貫之。(對於那些會用到你代碼的程序員,一以貫之的命名法簡直就是天降福音)
- 別用雙關語
- 避免將同一單詞用於不同目的。同一術語用於不同概念,基本上就是雙關語了。
- 比如,在多個類中都有add方法,該方法通過增加或連接兩個現存值來獲得新值。假設要寫個新類,該類中有一個方法,把單個參數放到群集(collection)中。如果把這個方法叫做add,貌似和其他add方法保持了一致,但實際上語義卻不同,應該用insert或append之類詞來命名才對。(把該方法命名為add,就是雙關語了)
- 使用解決方案領域名稱
- 記住,只有程序員才會讀你的代碼。所以,盡管去用那些計算機科學術語、算法名、模式名、數學術語吧
- 比如,對於熟悉訪問者(VISITOR)模式的程序員來說,名稱AccountVisitor富有意義。(程序員要做太多技術性工作,給這些事取個技術性的名稱,通常是最靠譜的做法)
- 使用源自所涉問題領域的名稱
- 如果不能用程序員熟悉的術語來給手頭的工作命名,就采用從所涉問題領域而來的名稱吧。至少,負責維護代碼的程序員就能去請教領域專家了。
- 優秀的程序員和設計師:其工作之一就是分離解決方案領域和問題領域的概念。與所涉問題領域更為貼近的代碼,應當采用源自問題領域的名稱。
- 添加有意義的語境
- 很少有名稱是能自我說明的——多數都不能。反之,你需要用到良好命名的類、函數或名稱空間來放置名稱,給讀者提供語境。如果沒這么做,給名稱添加前綴就是最后一招了。
- 比如,對孤零零的一個state變量來說,可以添加前綴addrFirstName、addrLastName、addrState等,以此提供語境。至少,讀者會明白這些變量是某個更大結構的一部分。當然,更好的方案是創建名為Address的類。這樣,即便是編譯器也會知道這些變量隸屬某個更大的概念了。
- 語境的增強,也讓算法能夠通過分解為更小的函數而變得更為干凈利落。
-
//語境不明確的變量 private void printGuessStatistics(char candidate,int count){ String number; String verb; String pluralModifier; ... } //有語境的變量 //創建GuessStaticsMessage類,把三個變量做成該類的成員字段 public class GuessStaticsMessage{ String number; String verb; String pluralModifier; ... }
- 不要添加沒用的語境
- 設若有一個名為“加油站豪華版”(Gas Station Deluxe)的應用,在其中給每個類添加GSD前綴就不是什么好點子。
- 只要短命稱足夠清楚,就要比長名稱要好。
- 對於Address類的實體來說,accountAddress和customerAddress都是不錯的名稱,不過用在類名上就不太好了。Address是個好類名。如果需要與MAC地址、端口地址和Web地址相區別,我會考慮使用PostalAddress、MAC和URI。這樣的名稱更為准確,而精確正是命名的要點。
3、函數
- 短小
- 代碼塊和縮進:if語句、else語句、while語句等,其中的代碼塊應該只有一行。該行大抵應該是一個函數調用語句。這樣不但能保持函數短小,而且,因為塊內調用的函數擁有較具說明性的名稱,從而增加了文檔上的價值。
- 這也意味着函數不應該大到足以容納嵌套結構。所以,函數的縮進層不該多於一層或兩層。(這樣的函數易於閱讀和理解)
- 只做一件事
- 函數應該做一件事。做好這件事。只做這一件事。
- 問題在於很難知道那件該做的事是什么?(其實,有時候一件事也很容易被看作是三件事或很多具體細化的步驟)如果函數只是做了該函數名下同一抽象層上的步驟,則函數還是只做了一件事。
- 要判斷函數是否不止做了一件事,還有一個方法,就是看是否能再拆出一個函數,該函數不僅只是單純地重新詮釋其實現。
- 函數中的區段:只做一件事的函數無法被合理的切分為多個區段。(這也是函數做事太多的明顯征兆)
- 每個函數一個抽象層級
- 函數中混雜不同抽象層級,往往讓人迷惑。讀者可能無法判斷某個表達式是基礎概念還是細節。更惡劣的是,就像破損的窗戶,一旦細節與基礎概念混雜,更多的細節就會在函數中糾結起來。
- 自項向下讀代碼:向下規則。這是保持函數短小、確保只做一件事的要訣。讓代碼讀起來像是一系列自項向下的TO起頭段落是保持抽象層級協調一致的有效技巧。
- switch語句
- 問題:寫出短小的switch語句很難(包括if/else在內),寫出只做一件事的switch語句也很難,Switch天生要做N件事。
- 解決:利用多態,確保每個switch都埋藏在較低的抽象層級,而且永遠不重復。
- 如下代碼:將switch語句埋到抽象工廠底下,不讓任何人看到。該工廠使用switch語句為Employee的派生物創建適當的實體,而不同的函數,如calculatePay、isPayday和deliverPay等,則藉由Emplyee接口多態地接受派遣。
- 對於switch語句,(作者的)規矩是如果只出現一次,用於創建多態對象,而且隱藏在某個繼承關系中,在系統其他部分看不到,就還能容忍。當然也要就是論事,有時也會部分或全部違反這條規矩。
- 使用描述性的名稱
- 沃德原則:“如果每個例程都讓你感到深合己意,那就是整潔代碼。”函數越短小、功能越集中,就越便於取個好名字。
- 別害怕長名稱。長而具有描述性的名稱,要比短而令人費解的名稱好。長而具有描述性的名稱,要比描述性的長注釋好。
- 選擇描述性的名稱能理清你關於模塊的設計思路,並幫你改進之。追索好名稱,往往導致對代碼的改善重構。
- 命名方式要保持一致。使用與模塊名一脈相承的短語、名詞和動詞給函數命名。例如:includeSTeardownPages、includeSetuoPages、includeSuiteSetupPage等
- 函數參數
- 最理想的參數數量是零(零參數函數),其次是一(單參數函數),再次是二(雙參數函數),應盡量避免三(三參數函數)。有足夠特殊的理由才能用三個以上參數(多參數函數)——所以無論如何也不要這樣做。
- 閱讀模塊所講述的故事時,includeSetupPage()要比includeSetupPageInto(newPage-Content)易於理解。參數與函數名處在不同的抽象層級,它要求了解目前不是特別重要的細節(即那個Stringbuffer)
- 測試的角度:要編寫能確保參數的各種組合運行正常的測試用例,是一件非常困難的事情。
- 輸出參數比輸入參數還要難以理解。
- 無副作用
- 副作用是一種謊言。函數承諾只做一件事,但還是會做其他被藏起來的事。有時,它會對自己類中的變量做出未能預期的改動。有時,它會對自己類中的變量做出未能預期的改動。有時,它會把變量搞成向函數傳遞的參數或是系統全局變量。無論哪種情況,都是具有破壞性的,會導致古怪的時序性耦合及順序依賴。(如果一定要時序性耦合,就應該在函數名稱里說明)
- 輸出參數:參數多數會被自然而然地看作是函數的輸入。普遍而言,應避免使用輸出參數。如果函數必須要修改某種狀態,就修改所屬對象的狀態吧。
- 分隔指令與詢問
- 函數要么做什么事,要么回答什么事,但二者不可得兼。
- 函數應該修改某對象的狀態,或是返回該對象的有關信息。兩樣都干常會導致混亂。
- 使用異常替代返回錯誤碼
- 從指令式函數返回錯誤碼輕微違反了指令與詢問分隔的規則。它鼓勵了在if語句判斷中把指令當作表達式使用。另一方面,如果使用異常替代返回錯誤碼,錯誤處理代碼就能從主路徑代碼中分離出來,得到簡化。
- 抽離Try/Catch代碼塊:Try/Catch代碼塊丑陋不堪。它們搞亂了代碼結構,把錯誤處理與正常流程混為一談。最好把它的主體部分抽離出來,另外形成函數。如圖:
- 錯誤處理就是一件事:函數應該只做一件事,錯誤處理就是一件事,因此,處理錯誤的函數不該做其他事。
- 如何寫出這樣的函數
- 一開始都冗長而復雜。然后,會打磨,分解函數、修改名稱、消除重復。縮短和重新安置方法。有時還拆散類。同時保持測試通過。
- 並不從一開始就按照規則寫函數。一般沒人做得到。
4、注釋
- 注釋會撒謊。
- 注釋存在的越久,就離其所描述的代碼越遠,越來越變得全然錯誤。(原因很簡單,程序員不能堅持維護注釋)
- 注釋不能美化糟糕的代碼
- 帶有少量注釋的整潔而有表達力的代碼,要比帶有大量注釋的零碎而復雜的代碼像樣的多。
- 用代碼來闡述
- 很多時候,簡單到只需要創建一個描述與注釋所言同一事物的函數即可。
- 好注釋——唯一真正好的注釋是你想辦法不去寫的注釋
- 法律信息
- 提供信息的注釋
- 對意圖的解釋
- 闡釋
- 警示
- TODO注釋(一種程序員認為應該做,但由於某些原因目前還沒做的工作)
- 放大
- 公共API中的Javadoc
- 壞注釋
- 喃喃自語
- 多余的注釋
- 誤導性注釋
- 循規式注釋
- 日志性注釋
- 廢話注釋
- 可怕的廢話
- 能用函數或變量時就別用注釋
- 位置標記
- 括號后面的注釋(盡管這對於含有深度嵌套結構的長函數可能有意義,但只會給我們更願意編寫的短小、封裝的函數帶來混亂。如果你發現自己想標記右括號,其實應該做的是縮短函數)
- 歸屬與署名
- 注釋掉的代碼
- HTML注釋
- 非本地信息
- 信息過多
- 不明顯的聯系
- 函數頭
- 非公共代碼中的Javadoc
注:轉載請注明出處