【《代碼整潔之道》精讀與演繹】之四 優秀代碼的格式準則

這篇文章將與大家一起聊一聊,書寫代碼過程中一些良好的格式規範。



一、引言



以下引言的內容,有必要伴隨這個系列的每一次更新,這次也不例外。

 

《代碼整潔之道》這本書提出了一個觀點:代碼質量與其整潔度成正比,乾淨的代碼,既在質量上可靠,也爲後期維護、升級奠定了良好基礎。書中介紹的規則均來自作者多年的實踐經驗,涵蓋從命名到重構的多個編程方面,雖爲一“家”之言,然誠有可資借鑑的價值。

 

但我們知道,很多時候,理想很豐滿,現實很骨感,也知道人在江湖,身不由己。因爲項目的緊迫性,需求的多樣性,我們無法時時刻刻都寫出整潔的代碼,保持自己輸出的都是高質量、優雅的代碼。

 

但若我們理解了代碼整潔之道的精髓,我們會知道怎樣讓自己的代碼更加優雅、整潔、易讀、易擴展,知道真正整潔的代碼應該是怎麼樣的,也許就會漸漸養成持續輸出整潔代碼的習慣。

 

而且或許你會發現,若你一直保持輸出整潔代碼的習慣,長期來看,會讓你的整體效率和代碼質量大大提升。

 



 

二、本文涉及知識點思維導圖




國際慣例,先放出這篇文章所涉及內容知識點的一張思維導圖,就開始正文。大家若是疲於閱讀文章正文,直接看這張圖,也是可以Get到本文的主要知識點的大概。








三、優秀代碼的書寫格式準則

 



1 像報紙一樣一目瞭然


想想那些閱讀量巨大的報紙文章。你從上到下閱讀。在頂部,你希望有個頭條,告訴你故事主題,好讓你決定是否要讀下去。第一段是整個故事的大綱,給出粗線條概述,但隱藏了故事細節。接着讀下去,細節漸次增加,直至你瞭解所有的日期、名字、引語、說話及其他細節。

 

優秀的源文件也要像報紙文章一樣。名稱應當簡單並且一目瞭然,名稱本身應該足夠告訴我們是否在正確的模塊中。源文件最頂部應該給出高層次概念和算法。細節應該往下漸次展開,直至找到源文件中最底層的函數和細節。

 


 

恰如其分的註釋


帶有少量註釋的整潔而有力的代碼,比帶有大量註釋的零碎而複雜的代碼更加優秀。

我們知道,註釋是爲代碼服務的,註釋的存在大多數原因是爲了代碼更加易讀,但註釋並不能美化糟糕的代碼。

 

另外,注意一點。註釋存在的時間越久,就會離其所描述的代碼的意義越遠,越來越變得全然錯誤,因爲大多數程序員們不能堅持(或者因爲忘了)去維護註釋。


當然,教學性質的代碼,多半是註釋越詳細越好。

 



合適的單文件行數

 

儘可能用幾百行以內的單文件來構造出出色的系統,因爲短文件通常比長文件更易於理解。當然,和之前的一些準則一樣,只是提供大方向,並非不可違背。


例如,《代碼整潔之道》第五章中提到的FitNess系統,就是由大多數爲200行、最長500行的單個文件來構造出總長約5萬行的出色系統。


 

 

合理地利用空白行


古詩中有留白,代碼的書寫中也要有適當的留白,也就是空白行。

在每個命名空間、類、函數之間,都需用空白行隔開(應該大家在學編程之初,就早有遵守)。這條極其簡單的規則極大地影響到了代碼的視覺外觀。每個空白行都是一條線索,標識出新的獨立概念。

其實,在往下讀代碼時,你會發現你的目光總停留於空白行之後的那一行。用空白行隔開每個命名空間、類、函數,代碼的可讀性會大大提升。


 


讓緊密相關的代碼相互靠近


如果說空白行隔開了概念,那麼靠近的代碼行則暗示了他們之間的緊密聯繫。所以,緊密相關的代碼應該相互靠近。

 

舉個反例(代碼段1):

[csharp] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. public class ReporterConfig  
  2. {  
  3.     /** 
  4.     * The class name of the reporter listener 
  5.     */  
  6.     private String m_className;  
  7.   
  8.     /** 
  9.     * The properties of the reporter listener 
  10.     */  
  11.     private List<Property> m_properties = new ArrayList<Property>();  
  12.   
  13.     public void addProperty(Property property)  
  14.     {  
  15.         m_properties.add(property);  
  16.     }  
  17. }  

再看個正面示例(代碼段2):

  1. public class ReporterConfig  
  2. {  
  3.     private String m_className;  
  4.     private List<Property> m_properties = new ArrayList<Property>();  
  5.   
  6.     public void addProperty(Property property)  
  7.     {  
  8.         m_properties.add(property);  
  9.     }  
  10. }  

以上這段正面示例(代碼段2)比反例(代碼段1)中的代碼好太多,它正好一覽無遺,一眼就能看這個是有兩個變量和一個方法的類。而再看看反例,註釋簡直畫蛇添足,隔斷了兩個實體變量間的聯繫,我們不得不移動頭部和眼球,才能獲得相同的理解度。





基於關聯的代碼分佈


關係密切的概念應該相互靠近。對於那些關係密切、放置於同一源文件中的概念,他們之間的區隔應該成爲對相互的易懂度有多重要的衡量標準。應該避免迫使讀者在源文件和類中跳來跳去。變量的聲明應儘可能靠近其使用位置。

對於大多數短函數,函數中的本地變量應當在函數的頂部出現。例如如下代碼中的is變量的聲明:

  1. private static void readPreferences()  
  2. {  
  3.     InputStream is= null;  
  4.     try  
  5.     {  
  6.         is= new FileInputStream(getPreferencesFile());  
  7.         setPreferences(new Properties(getPreferences()));  
  8.         getPreferences().load(is);  
  9.     }  
  10.     catch (IOException e)  
  11.     {  
  12.         DoSomeThing();  
  13.     }  
  14. }  


而循環中的控制變量應該總在循環語句中聲明,例如如下代碼中each變量的聲明:

  1. public int countTestCases()  
  2. {  
  3.     int count = 0;  
  4.     for (Test each : tests)  
  5.         count += each.countTestCases();  
  6.     return count;  
  7. }  

在某些較長的函數中,變量也可能在某代碼塊的頂部,或在循環之前聲明。例如如下代碼中tr變量的聲明:

  1. ...  
  2. for (XmlTest test : m_suite.getTests())   
  3. {  
  4.     TestRunner tr = m_runnerFactory.newTestRunner(this, test);  
  5.     tr.addListener(m_textReporter);  
  6.     m_testRunners.add(tr);  
  7.     invoker = tr.getInvoker();  
  8.     for (ITestNGMethod m : tr.getBeforeSuiteMethods())   
  9.     {  
  10.         beforeSuiteMethods.put(m.getMethod(), m);  
  11.     }  
  12.     for (ITestNGMethod m : tr.getAfterSuiteMethods())  
  13.     {  
  14.         afterSuiteMethods.put(m.getMethod(), m);  
  15.     }  
  16. }  
  17. ...  

另外,實體變量應當在類的頂部聲明(也有一些流派喜歡將實體變量放到類的底部)。

若某個函數調用了另一個,就應該把它們放到一起,而且調用者應該儘量放到被調用者上面。這樣,程序就有自然的順序。若堅定地遵守這條約定,讀者將能夠確信函數聲明總會在其調用後很快出現。

概念相關的代碼應該放到一起。相關性越強,則彼此之間的距離就該越短。

 

這一節的要點整理一下,大致就是:

  • 變量的聲明應儘可能靠近其使用位置。
  • 循環中的控制變量應該在循環語句中聲明。
  • 短函數中的本地變量應當在函數的頂部聲明。
  • 而對於某些長函數,變量也可以在某代碼塊的頂部,或在循環之前聲明。
  • 實體變量應當在類的頂部聲明。
  • 若某個函數調用了另一個,就應該把它們放到一起,而且調用者應該儘量放到被調用者上面。
  • 概念相關的代碼應該放到一起。相關性越強,則彼此之間的距離就該越短。

 



團隊遵從同一套代碼規範

 

一個好的團隊應當約定與遵從一套代碼規範,並且每個成員都應當採用此風格。我們希望一個項目中的代碼擁有相似甚至相同的風格,像默契無間的團隊所完成的藝術品,而不是像一大票意見相左的個人所堆砌起來的殘次品。


定製一套編碼與格式風格不需要太多時間,但對整個團隊代碼風格統一性的提升,卻是立竿見影的。


記住,好的軟件系統是由一系列風格一致的代碼文件組成的。儘量不要用各種不同的風格來構成一個項目的各個部分,這樣會增加項目本身的複雜度與混亂程度。

 





四、範例代碼

 



和上篇文章一樣,有必要貼出一段書中推崇的整潔代碼作爲本次代碼書寫格式的範例。書中的這段代碼採用java語言,但絲毫不影響使用C++和C#的朋友們閱讀。

  1. public class CodeAnalyzer implements JavaFileAnalysis  
  2. {  
  3.     private int lineCount;  
  4.     private int maxLineWidth;  
  5.     private int widestLineNumber;  
  6.     private LineWidthHistogram lineWidthHistogram;  
  7.     private int totalChars;  
  8.   
  9.     public CodeAnalyzer()  
  10.     {  
  11.         lineWidthHistogram = new LineWidthHistogram();  
  12.     }  
  13.   
  14.     public static List<File> findJavaFiles(File parentDirectory)  
  15.     {  
  16.         List<File> files = new ArrayList<File>();  
  17.         findJavaFiles(parentDirectory, files);  
  18.         return files;  
  19.     }  
  20.   
  21.     private static void findJavaFiles(File parentDirectory, List<File> files)  
  22.     {  
  23.         for (File file : parentDirectory.listFiles())  
  24.         {  
  25.             if (file.getName().endsWith(".java"))  
  26.                 files.add(file);  
  27.             else if (file.isDirectory())  
  28.                 findJavaFiles(file, files);  
  29.         }  
  30.     }  
  31.   
  32.     public void analyzeFile(File javaFile) throws Exception  
  33.     {  
  34.         BufferedReader br = new BufferedReader(new FileReader(javaFile));  
  35.         String line;  
  36.         while ((line = br.readLine()) != null)  
  37.         measureLine(line);  
  38.     }  
  39.   
  40.     private void measureLine(String line)  
  41.     {  
  42.         lineCount++;  
  43.         int lineSize = line.length();  
  44.         totalChars += lineSize;  
  45.         lineWidthHistogram.addLine(lineSize, lineCount);  
  46.         recordWidestLine(lineSize);  
  47.     }  
  48.   
  49.     private void recordWidestLine(int lineSize)  
  50.     {  
  51.         if (lineSize > maxLineWidth)  
  52.         {  
  53.             maxLineWidth = lineSize;  
  54.             widestLineNumber = lineCount;  
  55.         }  
  56.     }  
  57.   
  58.     public int getLineCount()  
  59.     {  
  60.         return lineCount;  
  61.     }  
  62.   
  63.     public int getMaxLineWidth()  
  64.     {  
  65.         return maxLineWidth;  
  66.     }  
  67.   
  68.     public int getWidestLineNumber()  
  69.     {  
  70.         return widestLineNumber;  
  71.     }  
  72.   
  73.     public LineWidthHistogram getLineWidthHistogram()  
  74.     {  
  75.         return lineWidthHistogram;  
  76.     }  
  77.   
  78.     public double getMeanLineWidth()  
  79.     {  
  80.         return (double)totalChars / lineCount;  
  81.     }  
  82.   
  83.     public int getMedianLineWidth()  
  84.     {  
  85.         Integer[] sortedWidths = getSortedWidths();  
  86.         int cumulativeLineCount = 0;  
  87.         for (int width : sortedWidths)  
  88.         {  
  89.             cumulativeLineCount += lineCountForWidth(width);  
  90.             if (cumulativeLineCount > lineCount / 2)  
  91.                 return width;  
  92.         }  
  93.         throw new Error("Cannot get here");  
  94.     }  
  95.   
  96.     private int lineCountForWidth(int width)  
  97.     {  
  98.         return lineWidthHistogram.getLinesforWidth(width).size();  
  99.     }  
  100.   
  101.     private Integer[] getSortedWidths()  
  102.     {  
  103.         Set<Integer> widths = lineWidthHistogram.getWidths();  
  104.         Integer[] sortedWidths = (widths.toArray(new Integer[0]));  
  105.         Arrays.sort(sortedWidths);  
  106.         return sortedWidths;  
  107.     }  
  108. }  






五、小結:讓代碼不僅僅是能工作



代碼的格式關乎溝通,而溝通是專業開發者的頭等大事,所以良好代碼的格式至關重要。


或許之前我們認爲“讓代碼能工作”纔是專業開發者的頭等大事。然而,《代碼整潔之道》這本書,希望我們能拋棄這個觀點。


讓代碼能工作確實是代碼存在的首要意義,但作爲一名有追求的程序員,請你想一想,今天你編寫的功能,極可能在下一版中被修改,但代碼的可讀性卻會對以後可能發生的修改行爲產生深遠影響。原始代碼修改之後很久,其代碼風格和可讀性仍會影響到可維護性和擴展性。即便代碼已不復存在,你的風格和律條仍影響深遠。

 

“當有人在閱讀我們的代碼時,我們希望他們能爲其整潔性、一致性和優秀的細節處理而震驚。我們希望他們高高揚起眉毛,一路看下去,希望他們感受能到那些爲之勞作的專業人士們的優秀職業素養。但若他們看到的只是一堆由酒醉的水手寫出的鬼畫符,那他們多半會得出結論——這個項目的其他部分應該也是混亂不堪的。”

 

所以,各位,在開發過程中請不要僅僅是停留在“讓代碼可以工作”的層面,而更要注重自身輸出代碼的可維護性與擴展性。請做一個更有追求的程序員。




 

六、本文涉及知識點提煉整理



整潔代碼的書寫格式,可以遵從如下幾個原則:

 

第一原則:像報紙一樣一目瞭然。優秀的源文件也要像報紙文章一樣,名稱應當簡單並且一目瞭然,名稱本身應該足夠告訴我們是否在正確的模塊中。源文件最頂部應該給出高層次概念和算法。細節應該往下漸次展開,直至找到源文件中最底層的函數和細節。

第二原則:恰如其分的註釋。帶有少量註釋的整潔而有力的代碼,比帶有大量註釋的零碎而複雜的代碼更加優秀。

第三原則:合適的單文件行數。儘可能用幾百行以內的單文件來構造出出色的系統,因爲短文件通常比長文件更易於理解。

第四原則:合理地利用空白行。在每個命名空間、類、函數之間,都需用空白行隔開。

第五原則:讓緊密相關的代碼相互靠近。靠近的代碼行暗示着他們之間的緊密聯繫。所以,緊密相關的代碼應該相互靠近。

第六原則:基於關聯的代碼分佈。

  • 變量的聲明應儘可能靠近其使用位置。
  • 循環中的控制變量應該在循環語句中聲明。
  • 短函數中的本地變量應當在函數的頂部聲明。
  • 對於某些長函數,變量也可以在某代碼塊的頂部,或在循環之前聲明。
  • 實體變量應當在類的頂部聲明。
  • 若某個函數調用了另一個,就應該把它們放到一起,而且調用者應該儘量放到被調用者上面。
  • 概念相關的代碼應該放到一起。相關性越強,則彼此之間的距離就該越短。

第七原則:團隊遵從同一套代碼規範。一個好的團隊應當約定與遵從一套代碼規範,並且每個成員都應當採用此風格。

 




本文就此結束。

下篇文章,我們將繼續《代碼整潔之道》的精讀與演繹,探討更多的內容。

Best Wish~

發佈了18 篇原創文章 · 獲贊 4 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章