這篇文章將與大家一起聊一聊,書寫代碼過程中一些良好的格式規(guī)范。 一、引言 以下引言的內(nèi)容,有必要伴隨這個系列的每一次更新,這次也不例外。 《代碼整潔之道》這本書提出了一個觀點:代碼質(zhì)量與其整潔度成正比,干凈的代碼,既在質(zhì)量上可靠,也為后期維護、升級奠定了良好基礎(chǔ)。書中介紹的規(guī)則均來自作者多年的實踐經(jīng)驗,涵蓋從命名到重構(gòu)的多個編程方面,雖為一“家”之言,然誠有可資借鑒的價值。 但我們知道,很多時候,理想很豐滿,現(xiàn)實很骨感,也知道人在江湖,身不由己。因為項目的緊迫性,需求的多樣性,我們無法時時刻刻都寫出整潔的代碼,保持自己輸出的都是高質(zhì)量、優(yōu)雅的代碼。 但若我們理解了代碼整潔之道的精髓,我們會知道怎樣讓自己的代碼更加優(yōu)雅、整潔、易讀、易擴展,知道真正整潔的代碼應(yīng)該是怎么樣的,也許就會漸漸養(yǎng)成持續(xù)輸出整潔代碼的習(xí)慣。 而且或許你會發(fā)現(xiàn),若你一直保持輸出整潔代碼的習(xí)慣,長期來看,會讓你的整體效率和代碼質(zhì)量大大提升。 二、本文涉及知識點思維導(dǎo)圖 國際慣例,先放出這篇文章所涉及內(nèi)容知識點的一張思維導(dǎo)圖,就開始正文。大家若是疲于閱讀文章正文,直接看這張圖,也是可以Get到本文的主要知識點的大概。 三、優(yōu)秀代碼的書寫格式準(zhǔn)則 1 像報紙一樣一目了然 想想那些閱讀量巨大的報紙文章。你從上到下閱讀。在頂部,你希望有個頭條,告訴你故事主題,好讓你決定是否要讀下去。第一段是整個故事的大綱,給出粗線條概述,但隱藏了故事細(xì)節(jié)。接著讀下去,細(xì)節(jié)漸次增加,直至你了解所有的日期、名字、引語、說話及其他細(xì)節(jié)。 優(yōu)秀的源文件也要像報紙文章一樣。名稱應(yīng)當(dāng)簡單并且一目了然,名稱本身應(yīng)該足夠告訴我們是否在正確的模塊中。源文件最頂部應(yīng)該給出高層次概念和算法。細(xì)節(jié)應(yīng)該往下漸次展開,直至找到源文件中最底層的函數(shù)和細(xì)節(jié)。 2 恰如其分的注釋 帶有少量注釋的整潔而有力的代碼,比帶有大量注釋的零碎而復(fù)雜的代碼更加優(yōu)秀。 我們知道,注釋是為代碼服務(wù)的,注釋的存在大多數(shù)原因是為了代碼更加易讀,但注釋并不能美化糟糕的代碼。 另外,注意一點。注釋存在的時間越久,就會離其所描述的代碼的意義越遠,越來越變得全然錯誤,因為大多數(shù)程序員們不能堅持(或者因為忘了)去維護注釋。 當(dāng)然,教學(xué)性質(zhì)的代碼,多半是注釋越詳細(xì)越好。 3 合適的單文件行數(shù) 盡可能用幾百行以內(nèi)的單文件來構(gòu)造出出色的系統(tǒng),因為短文件通常比長文件更易于理解。當(dāng)然,和之前的一些準(zhǔn)則一樣,只是提供大方向,并非不可違背。 例如,《代碼整潔之道》第五章中提到的FitNess系統(tǒng),就是由大多數(shù)為200行、最長500行的單個文件來構(gòu)造出總長約5萬行的出色系統(tǒng)。 4 合理地利用空白行 古詩中有留白,代碼的書寫中也要有適當(dāng)?shù)牧舭祝簿褪强瞻仔小?br /> 在每個命名空間、類、函數(shù)之間,都需用空白行隔開(應(yīng)該大家在學(xué)編程之初,就早有遵守)。這條極其簡單的規(guī)則極大地影響到了代碼的視覺外觀。每個空白行都是一條線索,標(biāo)識出新的獨立概念。 其實,在往下讀代碼時,你會發(fā)現(xiàn)你的目光總停留于空白行之后的那一行。用空白行隔開每個命名空間、類、函數(shù),代碼的可讀性會大大提升。 5 讓緊密相關(guān)的代碼相互靠近 如果說空白行隔開了概念,那么靠近的代碼行則暗示了他們之間的緊密聯(lián)系。所以,緊密相關(guān)的代碼應(yīng)該相互靠近。 舉個反例(代碼段1): [csharp] view plain copy print? public class ReporterConfig { /** * The class name of the reporter listener */ private String m_className; /** * The properties of the reporter listener */ private List m_properties = new ArrayList (); public void addProperty(Property property) { m_properties.add(property); } } 再看個正面示例(代碼段2): [cpp] view plain copy print? public class ReporterConfig { private String m_className; private List m_properties = new ArrayList (); public void addProperty(Property property) { m_properties.add(property); } } 以上這段正面示例(代碼段2)比反例(代碼段1)中的代碼好太多,它正好一覽無遺,一眼就能看這個是有兩個變量和一個方法的類。而再看看反例,注釋簡直畫蛇添足,隔斷了兩個實體變量間的聯(lián)系,我們不得不移動頭部和眼球,才能獲得相同的理解度。 6 基于關(guān)聯(lián)的代碼分布 關(guān)系密切的概念應(yīng)該相互靠近。對于那些關(guān)系密切、放置于同一源文件中的概念,他們之間的區(qū)隔應(yīng)該成為對相互的易懂度有多重要的衡量標(biāo)準(zhǔn)。應(yīng)該避免迫使讀者在源文件和類中跳來跳去。變量的聲明應(yīng)盡可能靠近其使用位置。 對于大多數(shù)短函數(shù),函數(shù)中的本地變量應(yīng)當(dāng)在函數(shù)的頂部出現(xiàn)。例如如下代碼中的is變量的聲明: [cpp] view plain copy print? private static void readPreferences() { InputStream is= null; try { is= new FileInputStream(getPreferencesFile()); setPreferences(new Properties(getPreferences())); getPreferences().load(is); } catch (IOException e) { DoSomeThing(); } } 而循環(huán)中的控制變量應(yīng)該總在循環(huán)語句中聲明,例如如下代碼中each變量的聲明: [cpp] view plain copy print? public int countTestCases() { int count = 0; for (Test each : tests) count += each.countTestCases(); return count; } 在某些較長的函數(shù)中,變量也可能在某代碼塊的頂部,或在循環(huán)之前聲明。例如如下代碼中tr變量的聲明: [cpp] view plain copy print? ... for (XmlTest test : m_suite.getTests()) { TestRunner tr = m_runnerFactory.newTestRunner(this, test); tr.addListener(m_textReporter); m_testRunners.add(tr); invoker = tr.getInvoker(); for (ITestNGMethod m : tr.getBeforeSuiteMethods()) { beforeSuiteMethods.put(m.getMethod(), m); } for (ITestNGMethod m : tr.getAfterSuiteMethods()) { afterSuiteMethods.put(m.getMethod(), m); } } ... 另外,實體變量應(yīng)當(dāng)在類的頂部聲明(也有一些流派喜歡將實體變量放到類的底部)。 若某個函數(shù)調(diào)用了另一個,就應(yīng)該把它們放到一起,而且調(diào)用者應(yīng)該盡量放到被調(diào)用者上面。這樣,程序就有自然的順序。若堅定地遵守這條約定,讀者將能夠確信函數(shù)聲明總會在其調(diào)用后很快出現(xiàn)。 概念相關(guān)的代碼應(yīng)該放到一起。相關(guān)性越強,則彼此之間的距離就該越短。 這一節(jié)的要點整理一下,大致就是: 變量的聲明應(yīng)盡可能靠近其使用位置。 循環(huán)中的控制變量應(yīng)該在循環(huán)語句中聲明。 短函數(shù)中的本地變量應(yīng)當(dāng)在函數(shù)的頂部聲明。 而對于某些長函數(shù),變量也可以在某代碼塊的頂部,或在循環(huán)之前聲明。 實體變量應(yīng)當(dāng)在類的頂部聲明。 若某個函數(shù)調(diào)用了另一個,就應(yīng)該把它們放到一起,而且調(diào)用者應(yīng)該盡量放到被調(diào)用者上面。 概念相關(guān)的代碼應(yīng)該放到一起。相關(guān)性越強,則彼此之間的距離就該越短。 7 團隊遵從同一套代碼規(guī)范 一個好的團隊?wèi)?yīng)當(dāng)約定與遵從一套代碼規(guī)范,并且每個成員都應(yīng)當(dāng)采用此風(fēng)格。我們希望一個項目中的代碼擁有相似甚至相同的風(fēng)格,像默契無間的團隊所完成的藝術(shù)品,而不是像一大票意見相左的個人所堆砌起來的殘次品。 定制一套編碼與格式風(fēng)格不需要太多時間,但對整個團隊代碼風(fēng)格統(tǒng)一性的提升,卻是立竿見影的。 記住,好的軟件系統(tǒng)是由一系列風(fēng)格一致的代碼文件組成的。盡量不要用各種不同的風(fēng)格來構(gòu)成一個項目的各個部分,這樣會增加項目本身的復(fù)雜度與混亂程度。 四、范例代碼 和上篇文章一樣,有必要貼出一段書中推崇的整潔代碼作為本次代碼書寫格式的范例。書中的這段代碼采用java語言,但絲毫不影響使用C++和C#的朋友們閱讀。 [cpp] view plain copy print? public class CodeAnalyzer implements JavaFileAnalysis { private int lineCount; private int maxLineWidth; private int widestLineNumber; private LineWidthHistogram lineWidthHistogram; private int totalChars; public CodeAnalyzer() { lineWidthHistogram = new LineWidthHistogram(); } public static List findJavaFiles(File parentDirectory) { List files = new ArrayList(); findJavaFiles(parentDirectory, files); return files; } private static void findJavaFiles(File parentDirectory, List files) { for (File file : parentDirectory.listFiles()) { if (file.getName().endsWith(".java")) files.add(file); else if (file.isDirectory()) findJavaFiles(file, files); } } public void analyzeFile(File javaFile) throws Exception { BufferedReader br = new BufferedReader(new FileReader(javaFile)); String line; while ((line = br.readLine()) != null) measureLine(line); } private void measureLine(String line) { lineCount++; int lineSize = line.length(); totalChars += lineSize; lineWidthHistogram.addLine(lineSize, lineCount); recordWidestLine(lineSize); } private void recordWidestLine(int lineSize) { if (lineSize > maxLineWidth) { maxLineWidth = lineSize; widestLineNumber = lineCount; } } public int getLineCount() { return lineCount; } public int getMaxLineWidth() { return maxLineWidth; } public int getWidestLineNumber() { return widestLineNumber; } public LineWidthHistogram getLineWidthHistogram() { return lineWidthHistogram; } public double getMeanLineWidth() { return (double)totalChars / lineCount; } public int getMedianLineWidth() { Integer[] sortedWidths = getSortedWidths(); int cumulativeLineCount = 0; for (int width : sortedWidths) { cumulativeLineCount += lineCountForWidth(width); if (cumulativeLineCount > lineCount / 2) return width; } throw new Error("Cannot get here"); } private int lineCountForWidth(int width) { return lineWidthHistogram.getLinesforWidth(width).size(); } private Integer[] getSortedWidths() { Set widths = lineWidthHistogram.getWidths(); Integer[] sortedWidths = (widths.toArray(new Integer[0])); Arrays.sort(sortedWidths); return sortedWidths; } } 五、小結(jié):讓代碼不僅僅是能工作 代碼的格式關(guān)乎溝通,而溝通是專業(yè)開發(fā)者的頭等大事,所以良好代碼的格式至關(guān)重要。 或許之前我們認(rèn)為“讓代碼能工作”才是專業(yè)開發(fā)者的頭等大事。然而,《代碼整潔之道》這本書,希望我們能拋棄這個觀點。 讓代碼能工作確實是代碼存在的首要意義,但作為一名有追求的程序員,請你想一想,今天你編寫的功能,極可能在下一版中被修改,但代碼的可讀性卻會對以后可能發(fā)生的修改行為產(chǎn)生深遠影響。原始代碼修改之后很久,其代碼風(fēng)格和可讀性仍會影響到可維護性和擴展性。即便代碼已不復(fù)存在,你的風(fēng)格和律條仍影響深遠。 “當(dāng)有人在閱讀我們的代碼時,我們希望他們能為其整潔性、一致性和優(yōu)秀的細(xì)節(jié)處理而震驚。我們希望他們高高揚起眉毛,一路看下去,希望他們感受能到那些為之勞作的專業(yè)人士們的優(yōu)秀職業(yè)素養(yǎng)。但若他們看到的只是一堆由酒醉的水手寫出的鬼畫符,那他們多半會得出結(jié)論——這個項目的其他部分應(yīng)該也是混亂不堪的! 所以,各位,在開發(fā)過程中請不要僅僅是停留在“讓代碼可以工作”的層面,而更要注重自身輸出代碼的可維護性與擴展性。請做一個更有追求的程序員。 六、本文涉及知識點提煉整理 整潔代碼的書寫格式,可以遵從如下幾個原則: 第一原則:像報紙一樣一目了然。優(yōu)秀的源文件也要像報紙文章一樣,名稱應(yīng)當(dāng)簡單并且一目了然,名稱本身應(yīng)該足夠告訴我們是否在正確的模塊中。源文件最頂部應(yīng)該給出高層次概念和算法。細(xì)節(jié)應(yīng)該往下漸次展開,直至找到源文件中最底層的函數(shù)和細(xì)節(jié)。 第二原則:恰如其分的注釋。帶有少量注釋的整潔而有力的代碼,比帶有大量注釋的零碎而復(fù)雜的代碼更加優(yōu)秀。 第三原則:合適的單文件行數(shù)。盡可能用幾百行以內(nèi)的單文件來構(gòu)造出出色的系統(tǒng),因為短文件通常比長文件更易于理解。 第四原則:合理地利用空白行。在每個命名空間、類、函數(shù)之間,都需用空白行隔開。 第五原則:讓緊密相關(guān)的代碼相互靠近。靠近的代碼行暗示著他們之間的緊密聯(lián)系。所以,緊密相關(guān)的代碼應(yīng)該相互靠近。 第六原則:基于關(guān)聯(lián)的代碼分布。 變量的聲明應(yīng)盡可能靠近其使用位置。 循環(huán)中的控制變量應(yīng)該在循環(huán)語句中聲明。 短函數(shù)中的本地變量應(yīng)當(dāng)在函數(shù)的頂部聲明。 對于某些長函數(shù),變量也可以在某代碼塊的頂部,或在循環(huán)之前聲明。 實體變量應(yīng)當(dāng)在類的頂部聲明。 若某個函數(shù)調(diào)用了另一個,就應(yīng)該把它們放到一起,而且調(diào)用者應(yīng)該盡量放到被調(diào)用者上面。 概念相關(guān)的代碼應(yīng)該放到一起。相關(guān)性越強,則彼此之間的距離就該越短。 第七原則:團隊遵從同一套代碼規(guī)范。一個好的團隊?wèi)?yīng)當(dāng)約定與遵從一套代碼規(guī)范,并且每個成員都應(yīng)當(dāng)采用此風(fēng)格。 |