概述 1、本文檔的內容主要來源于書籍《代碼整潔之道》作者Robert C.Martin,屬于讀書筆記。 2、軟件質量,不僅依賴于架構和項目管理,而且與代碼質量緊密相關,本書提出一種,代碼質量與整潔成正比的觀點,并給出了一系列行之有效的整潔代碼操作實踐,只要遵循這些規則,就可以編寫出整潔的代碼,從而提升代碼質量。 3、該書介紹的規則均來自于作者多年的實踐經驗,涵蓋從命名到重構的多個編程方面,具有很好的學習和借鑒價值。 4、習藝要有二:知和行。你應當學習有關規則、模式和實踐的知識,窮盡應知之事,并且對其了如指掌,通過刻苦實踐掌握它! 前言 學習整潔代碼很難,它不止于要求你掌握原則和模式,你還得在上面下功夫,并自行實踐,體驗失敗。你須觀察他人如何實踐與失敗,怎樣蹣跚學步,再轉頭學習他們的路數,。 本書要求你多用信息,多用功,而且非常用功。如何用功?-大量閱讀代碼,并琢磨代碼好在什么地方,壞在什么地方。 本書大概分為三部分:原則、模式和實踐。 一、 使用有意義的命名 1、名副其實 注意命名,一旦發現有更好的名稱就換掉舊的,這么做閱讀的人會更開心 名稱本身應該能解釋其含義,無需注釋就能看懂是最佳。比如 int d;//消失時間,以日計 int elapsedTimeInDays; 前者名稱沒有任何含義,在程序中使用時看不出這個變量的實際作用,需要對應注釋才能看懂,因此遠不如后者的名稱好! 2、避免誤導 程序員必須避免留下掩藏代碼本意的錯誤線索,避免使用與本意相悖的詞。 提防使用細節之處差別較小的名稱 使用相同的拼寫方式,前后拼寫不一致(大小寫不同),就是誤導。在使用編輯器名稱自動補全功能時,拼寫相近的變量容易引起誤選。 避免使用小寫字母l和大寫字母O作為變量名稱,易與1和0混淆 3、做有意義的區分 避免以數字系列命名,其無法提供正確的信息和導向作者意圖的線索。 不要使用意義相近的名稱,比如ProductInfo和ProductData變量不同,意思一樣,容易引起意義混淆 不要使用冗余信息,比如NameString,難道Name會是一個浮點數嗎?如果是,就不該使用Name命名。 4、使用讀的出來的名稱 人類善于記憶和使用單詞,如果名稱無法閱讀或者發音,就不是一個好名稱,討論和交流時也難以表達。 比如函數名稱為:genymdhms()//生成日期,年月日時分秒。 不要使用傻乎乎的自造詞,而要使用恰當的英語單詞 5、使用可搜索的名稱 比如字母e,f等就不是一個好的變量名,其是英文常用字母,不方便搜索, 單字母名稱僅限于本地局部變量使用,名稱長短應該與作用域大小相對應 如果程序中多出使用相同數字,實現相同功能,則需要使用宏定義變量代替。 比如WORK_DAYS_PER_WEEK就比數字5好搜索,也更能體現作者意圖 6、避免使用編碼 無需把類型和作用域編進名稱,這樣只會自找麻煩,既不便發音,也容易拼錯,對解決問題毫無幫助。 匈牙利標記法,破壞了不編碼的規則,不應該采用。 也不必使用成員前綴,應該把類和函數做的足夠小,同時使用可以高亮和顏色標出成員的編輯環境。(Keil,notepad++都支持)。 7、避免思維映射 不應當讓讀者把你腦中的名稱翻譯成他們熟知的名稱,這個問題常見于選擇使用問題領域的術語還是解決方案領域的術語時。 在作為局部變量時,并且名稱沒有沖突時,可以采用i,j,k作為循環變量。 專業程序員善用其能,編寫能讓他人理解的代碼 8、類名 類名應該是名稱或者名詞短語,例如Customer、Account,避免使用Manager、Data、Info這樣的類名,其不應該是動詞。 9、方法名 方法名應該是動詞或者動詞短語,比如postPayment、deletePage或s**e,屬性訪問應該加上set、get、is前綴 10、每個概念對應一個詞 給每個抽象概念選用一個詞,并且一以貫之。比如使用fetch、retrieve、get在多個類的中同種方法命名,就容易引起混淆。 11、別用雙關語 避免將以此用于不同目的,同一術語用于不同概念就是雙關語了。 比如多個類中都有add方法,該方法通過增加或者鏈接兩個現存值來獲得新值,如果一個新類的含義是,把單個參數放到群集(collection)中,使用add名稱,雖然保持了名稱一致,你是含義卻不同,應該使用insert才對。 12、使用解決方案領域的名稱 因為只有程序員才會讀取你的代碼,因此名稱應該選擇解決方案領域的名稱,而不是問題設計領域的名稱。比如名稱AccountVisitor就比JobQueue富有意義。 13、使用源自所涉及問題領域的名稱 當不能使用程序員所熟悉的術語命名時,就應該采用所涉及問題領域的名稱 與所涉問題領域更加貼近的代碼,應當采用源自問題領域的名稱 14、添加有意義的語境 很少有名稱能夠自我說明-多數都不能,因此需要使用良好命名的類、函數來放置名稱,給讀者提供語境。 比如添加前綴addrFirstName、addrLastName、addrState,就可以提供語境,這些變量屬于地址范圍。更好的做法是,創建一個名稱為Address的類,來存放這些相關變量。 語境的增強也讓算法能夠通過分解為更小的函數而變得干凈利索。 15、不要添加沒有意義的語境 比如應用(Gas Station Deluxe)簡稱為GSD,因此為每個函數、類、變量增加同樣的前綴就GSD命名就不是一個好點子。 Address是個好名稱,但是如果需要與MAC地址、端口地址或Web地址區分,應當使用PostalAddress、MAC、URI,這樣的名稱更為精確。 16、總結 取名字最難地方在于需要良好的描述技巧和共有的文化背景 試試上面的規則,看你的代碼的可讀性是否有所提升。如果維護別人的代碼,使用重構工具來解決問題,效果也好立竿見影,而且會持續下去。 二、關于函數的一些規則 1、短小 函數的第一規則就是短小。 代碼長度以20行封頂為最佳。 if,else,while語句的代碼塊應該只有一行,這樣不但能保持函數短小,還能增加可讀性。 函數的層級不應該多于兩層,這樣才容易理解和閱讀。 2、只做一件事 函數應該只做一件事,做好這一件事。 判斷函數是否只做了一件事的方法就是看其是否能夠拆出一個函數,該函數不是單純的重新詮釋其實現。 3、每個函數一個抽象層級 要確保函數做做一件事情,函數中的語句都要在同一抽象層級上。這是保持函數短小的要訣。 4、函數參數 最好少于3個,0個和1個最佳,3個以上請使用結構體代替。 輸出參數比輸入參數更加難以理解,讀函數時,習慣于認為信息通過參數輸入函數,通過返回值輸出。 給函數起個好名字,使其能夠很好的解釋函數的意圖、以及參數的順序。比如assertExpectedEqualsActual(expected,actual)就容易理解和記憶。 5、無副作用 副作用指的是被其隱藏起來的事情,比如開機檢查密碼函數,如果調用前密碼未經初始化,就會出錯,就隱藏了其時序性耦合的要求。應該在其函數中增加初始化密碼的功能,盡管違反了只做一件事的。 6、分割指令與查詢 函數修改某對象的狀態或者獲取該對象的參數,應該分為兩個函數實現。 比如函數設置某個指定屬性,如果成功就返回true,失敗返回false,用例如下: if(set("username","unclebob"))... 這樣的語句理解上面有歧義,set是動詞,但是在上下文中感覺像形容詞,從讀者角度看,這句語句的意思是問username的屬性是否已經設置為unclebob?還是詢問username的屬性是否成功設置為unclebob?因此最好改為如下所示: if(attributeExists("username")) { setAttribute("username","unclebob"); } 7、使用異常代替返回錯誤值 直接返回錯誤代碼鼓勵了在if語句中把指令當做表達式使用,會導致多層次嵌套結構。如果返回異常代替錯誤碼,錯誤處理代碼就可以從主路徑代碼中分離出來,得到簡化 8、不要重復 重復的代碼可維護性差,如果算法修改,所涉及的地方都要更新,代碼臃腫,還會增加放過錯誤的可能! 如果一個算法在不同函數中重復出現,則需要使用一個獨立的函數修復它。 面向對象和面向組件編程也多多少少都是消除重復的策略。 9、結構化編程 只要保持函數短小,偶爾出現return、break、continue語句沒有什么壞處,甚至比單入單出更具表達力。 goto語句只在大函數中才能用到,因此盡量避免使用。 10、如何寫出短小的函數 寫代碼和寫文章一樣,先寫出初稿、再細細打磨,直至達到心中的樣子。 初寫的函數,一般都是冗長而復雜,有太多縮進和嵌套循環,名稱隨意性大,代碼有重復。通過分解函數、修改名稱、消除重復,然后遵循本章的規則,重新組裝函數,就可以寫出短小精煉的函數。 三 關于注釋的規則 1、注釋不能美化糟糕的代碼 帶有少量注釋的整潔而有表達力的代碼,要比帶有大量注釋的零碎而復雜的代碼像樣的多! 與其花時間寫注釋,不如花時間清理早搞定糟糕的代碼。 代碼在變動、演化,而很不幸,注釋不是總隨之變動,時間越久,注釋離代碼的本意就越久。只有代碼是唯一真正準確的信息來源。。 2、用代碼闡述 使用代碼本身解釋其行為。 比如//Check to see if the employee is eligible for full benefits if((employee.flags &HOURLY_FLAG)&&(employee.age > 65)) 通過創建與注釋所言同一事物的函數即可。 if(employee.isEligibleForFullBenefits()) 3、好注釋 法律信息,比如公司代碼規范要求寫的有關法律的注釋,版權聲明等。 提供信息的注釋,更好的方法是使用函數名稱傳達信息。 對意圖的解釋:注釋不僅提供了有關實現的有用信息,而且還提供了某個決定后面的意圖。 闡釋:把晦澀難懂的參數或者返回值翻譯為可讀形式的注釋是有用的。 警示:用于警告其它程序員會出現某種后果的注釋也是有用的。 4、壞的注釋 喃喃自語式:只是覺得因為過程需要就增加的注釋。 多余的注釋:本身并不能比代碼提供更多的信息,就是多余。 誤導性注釋:含義不精確或者容易引起誤導的注釋。 循規性注釋:每個函數或者每個變量都要有注釋的規矩是全然可笑的,只會搞亂代碼。 日志式注釋:在有源碼控制系統的今天,這種冗長的記錄只會讓模塊變得凌亂不堪,應該刪除 廢話注釋:對顯然之事喋喋不休,毫無新意。看代碼的人員,也幾乎對其視而不見。因此用整理代碼的決心替代廢話的沖動,會使你成為更優秀的程序員。 可怕的廢話:在知名開源庫的J**adoc中,也可以看到復制黏貼錯誤,意思就是變量不一樣,注釋卻一樣。 位置標記:少用標記欄,比如//Action//////////////////////////////,特定函數放在這個下面,多數時候是在不必要。 括號后面的注釋:盡管對于多層嵌套有幫助,但是更應該使用短小、封裝的函數,如果你想標記右括號,其實應該做的是縮短函數。 歸屬與署名:源碼控制系統很善于記錄是誰在何時改動了什么,沒有必要用小小的簽名搞臟代碼。比如注釋/*Added by Rick*/ 注釋掉的代碼:直接把代碼注釋掉是討厭的做法,別人可能認為其一定有原因才留在這里不敢刪除,最后注釋掉的代碼像渣滓一樣堆在那里,別這么干! 信息過多的注釋:別再注釋中增加歷史性話題或者無關的細節描述。 不明顯的聯系:注釋的作用是解釋未能自我解釋的代碼,如果本身還需要解釋就沒有必要了。 函數頭:短函數不需要太多注釋,只需要寫一個好名字就比注釋強得多! 四 關于代碼格式的規則 你應該保持良好的代碼格式,選用一套管理代碼格式的簡單規則,然后貫徹實施。如果在團隊中工作,則團隊應該一致同意采用一套簡單的格式規則,所有成員都要遵從。使用能幫你應用這些格式規則的自動化工具會很有幫助。 1、格式的目的 原始代碼修改很久之后,其代碼風格和可讀性仍會影響到可維護性和擴展性。 代碼格式關乎溝通,而溝通時專業開發者的頭等大事! 哪些代碼格式方面能幫助我們溝通呢? 2、垂直格式 垂直尺寸 單個文件的長度尺寸與可理解性相關,統計數據表明,用大多數為200行到500行的單個文件可以構造出出色的系統,短文件通常比長文件容易理解! 函數布局 函數名稱應該足夠告訴我們是否在正確的模塊中,源文件頂部應該給出高層次的概念和算法,細節應該向下逐次展開,直至源文件中最低層的函數和細節。就像報紙一樣,標題統領提綱概要,內容逐次展開細節。 空行 在包聲明、函數之間、代碼塊之間應該使用空行隔開,這條簡單的規則極大的影響到代碼的視覺外觀。空白行作為一條線索,標示出獨立的概念。 靠近 如果說空白行隔開了概念,靠近的代碼則暗示了他們之間的緊密聯系。因此關系緊密的應該互相靠近. 垂直距離 全局變量聲明應該放到文件第一個函數聲明前,局部變量聲明放到函數頂部。相關函數應該放在一起,調用者放在被調用者上面,這樣就能輕易找到被調用函數,極大增強模塊的可讀性 3、橫向格式 行寬 一行代碼應該多寬?統計數據表明,70%的代碼行少于60個字符,代碼行應該盡量短小,死守80字節有點僵化,但是不要超過100字符或者120字符。簡單的規則是無需向右拖動滾動條,就可以看到全部代碼。 水平區隔與靠近 在運算符號兩端(乘號除外,因其優先級較高,多數格式化工具都忽視優先級)、函數名和右括號之間之間,函數參數之間增加空格! 水平對齊 經驗表明一組類聲明中的變量名、或者賦值語句的右值對齊沒有什么實際作用,因此左端對齊,使用同樣的空格區分規則即可。 縮進 有縮進的代碼結構,閱讀時可以很容易查找新的聲明、變量、類和函數塊,否則就需要折騰一番才能明白,程序員應該依賴縮進模式(在python中強制使用縮進表示for循環的范圍,C語言中使用大括號,但是也要依賴縮進增強可讀性)。 空范圍 有時while或for語句的語句體為空,如下所示.右端的分號很難看見,容易讓人迷惑。 while(dis.read(buf,0,readBufferSize) != 1); 因此最好改為如下格式 while(dis.read(buf,0,readBufferSize) != 1) { } 團隊規則 在團隊中,各成員應該采用一致的代碼格式,如果各自風格不同,會增加項目代碼的復雜度。 五 單元測試 過去10年來,編程專業領域進步很大,特別是敏捷和TDD(測試驅動開發)運動鼓舞了很多程序員編寫單元測試。TDD要我們在編寫生產代碼之前先編寫測試,其還有三大定律: 定律一:再編寫不能測試通過的測試驅動前,不可編寫生產代碼 定律二:只可編寫剛好無法通過的單元測試,不能編譯也不算通過 定律三:只可編寫剛好足以通過當前失敗測試的生產代碼。 這些定律要求測試與生產代碼一起編寫,測試代碼先寫,這樣寫程序,測試將覆蓋所有生產代碼。 1、保持測試的整潔 測試代碼和生產代碼一樣,需要被思考、設計和照料,像生產代碼一樣整潔 正是單元測試讓你的代碼可擴展、可維護、可復用。如果沒有測試,每次修改都可能帶來缺陷,無論架構如何有擴展性,設計劃分如何好,如果沒有單元測試,你的改動都可能帶來不可預知的缺陷。 2、整潔的測試 整潔的唯一要素的就是可讀性,在單元測試中,可讀性比生產代碼還重要。如何做到可讀性?就是要明確、簡潔和具有足夠的表達力。 測試要采用構造-操作-檢驗模式,每個測試均要可以清晰的拆分為三個環節,第一個環節構造測試數據,第二個環節操作測試數據,第三個環節檢驗操作是否得到期望的結果。 3、每個測試一個斷言 單個測試函數有且僅有一個斷言是個好準則。 每個測試做測試一個概念,如果一個測試測量三件事情,可能會導致遺漏。 4、整潔的5條規則 快速(fast),測試應該快速運行,你才能想要頻繁運行它,如果不頻繁運行,就不能盡早發現問題。 獨立(Independent),測試應該互相獨立,從而可以單獨運行每個測試。 可重復(Repeatable),測試應該在任何環境中重復通過,否則當前環境具備時,你也會無法運行測試。 自足驗證(Self-Validating),測試應該有布爾值輸出去,無論如何不應該查看日志文件來確認測試是否通過。 及時(Timely),測試應該及時編寫,如果在生產代碼之后編寫測試,你會發現代碼難以測試。 |