【整潔之道】如何寫出更整潔的代碼(上)

 

如何寫出更整潔的代碼

 

 

  代碼整潔之道不是銀彈,不會立竿見影的帶來收益。

  沒有任何犀利的武功招式,只有一些我個人異常推崇的代碼整潔之道的內功心法。它不會直接有效的提高你寫代碼的能力與速度,但是對於程序員的整個職業生涯必然會帶來意想不到的好處。

  如果你還是一個在校學生,或者是剛工作沒多久的“菜鳥”,那麼很有必要接觸一些這方面的知識的。很顯然,它會幫助你更快的適應企業級開發的要求。

 

1. 爲什麼需要代碼更整潔?

  在考慮代碼整潔的時候,我們需要明確的一個前提是,這裏不討論代碼的對錯。

  關於什麼是整潔的代碼,可能千人千面,但是關於爲什麼要寫出整潔的代碼是要達成共識的。

  如果今天需要出去約會,不管是男生女生一定會將自己梳妝打扮一番吧。如果是週末自己一個人宅在家裏呢?可能很多人都是不修邊幅的。這體現了人際交往中很重要的一點:當需要與別人接觸時,會注重自己的儀容。不管咱們的顏值高低,怎麼都還是得捯飭一番不是?對於代碼而言,我們應該抱有一樣的要求。如果只是自己寫的好玩的“用後即丟“的代碼,讓它邋遢點其實也沒什麼影響。但是如果是在公司與其他人一起開發維護的代碼庫呢?別人會看我們寫的代碼,我們也需要看別人寫的代碼。這樣咱們是不是應該也要把自己的代碼好好打扮一下,畢竟代碼就是咱麼的面子啊!

 

  想必,對於每一個看過幾本編程方面書籍的人,都看過這麼兩個說法:

  1) 代碼是寫給人看的。

  2) 開發的大部分時間都是在看代碼。

 

  我們可能在看別人的代碼的時候,會在心裏情不自禁的飆幾句WTF,以宣泄自己對別人難以理解的代碼的煩躁之情。但與此同時,別人也可能在心裏F着咱們的代碼。有感於此,是不是覺得自己應該寫出更整潔、更易讀的代碼?讓別人對自己的代碼”無F可說“。

 

  我們需要追求整潔的代碼,但是代碼需要整潔的什麼程度呢?抱歉,好像這是一個現階段還沒有被量化的問題。在我看來,是需要找到一個平衡點。對於那些之後再也不會用到的代碼,當然就不用花費大力氣去追求極致的整潔。而對於那些日常中需要用到的代碼,我想讓它們再怎麼整潔也不爲過。

 

2. 怎麼寫出整潔的代碼?

  以下將是來自於《代碼整潔之道》的方法論。

 

2.1 命名

  需要被命名的有:變量、函數、參數、類、包等。

  1. 名副其實。選擇體現本意的名稱能讓人更容易理解和修改代碼。都知道類名用名詞性的詞,函數名用動詞性的詞。除了這個最基本的要求之外,名字應該能體現領域概念。使用表意更準確的詞彙,而不要用模棱兩可的詞彙增加代碼模糊度。
  2. 避免誤導。應避免使用與本意相悖的詞。
    1. 不要使用不同之處較小的名稱。如 InformationToSet, InformationToSend ,這樣會增加區分它們的時間成本。此外,在使用IDE的快捷補全功能時,可能會使用相似但錯誤的變量名。如果有兩個類型爲 String 的變量,一個爲 XYZControlllerForEffcientHandlingOfStrings 另一個爲 XYZControllerForEffectientStorageOfStrings ,在使用補全功能時,一不小心就可能使用錯誤的變量名而導致錯誤。
    2. 以同樣的方式拼寫同樣的概念。也就是說,同一個概念如果前後使用不一樣的名字,就會產生誤導的反效果。
    3. 最應該避免的是使用小寫字母l和大寫字母O作爲變量名,它們與數字1和0極難區分。
  3. 做有意義的區分。
    1. 不要使用數字系列的命名來區分。對於如下方法簽名
      /*
              該方法簽名需要註釋的幫助來能理解參數 a1, a2 分別表示什麼意思
           */
          public static void copyChars(char[] a1, char[] a2);
      
          /*
              方法簽名“自說明”參數的含義
           */
          public static void copyChars(char[] source, char[] destination);
      

      說明一個好的名字,對整個方法可讀性的提供。

    2. 只要體現有意義的區分,廢話都是冗餘的。對於 getActiveAccount();getActiveAccounts(); 這兩個函數很難通過名字就明白它們之間的區別。
  4. 使用讀得出來的名字。受制於“poor”的英文詞彙,一般很少涉及這點。但毫無疑問,如果在與別人交流代碼的時候,能夠順暢的讀出每個變量、函數名,無疑會使得整個交流更加的高效集中。
  5. 使用可搜索的名字。使用單字母名稱和數字常量的問題在於,當需要全局搜索某個名字的時候,會出現很多的重複。就好像,如果你在一個文件中全局搜索字母"e",一般會出現很多結果(某個單詞中間包含的字母也會被搜索到),這樣會模糊我們的搜索結果。
    1. 長名稱勝於短名稱。單字母名稱用於短方法中的本地變量。
    2. 名稱長短應該與其作用域大小相對應。
  6. 避免使用編碼。不要把類型或作用域信息編碼進名稱裏。
  7. 每個概念對應一個詞。避免將多個單詞用於同一個目的。給每個抽象概念選擇一個詞,並從一而終。例如,在DAO層中查詢數據庫時,很多時候會出現 get, query 等表示查詢的詞,對於這種情況項目組內應該要保持統一。
  8. 別用雙關語。避免將同一個單詞用於不同的目的。代碼作者應該盡力寫出易於理解的代碼。
  9. 使用解決方案領域名稱。使用在編程領域裏大家都認同並接受的概念。比如,當使用訪問者模式時,在名稱中加上 Visitor 會給熟悉設計模式的更多的信息。
  10. 使用問題領域的名稱。也就是在名字中使用業務領域的概念來命名。這些概念都是從你的項目正在解決的問題中提煉出來的。在你的日常開發交流中會使用到的概念。
  11. 添加有意義的語境,不要添加沒有意義的語境。

  取名字最難的地方在於需要良好的描技巧和共有的文化背景。一般而言,很難一下就給所有的變量、函數取一個簡單直接、表意準確的名字,所以更應該的是不斷的重構改善它們。隨着對業務知識理解的不斷加深,我們會發現以前起的名字不那麼貼切,這個時候應該果斷的對其進行修改。

 

  大概就是像給自己家小孩起名字一樣的態度對待每個出現在代碼中的名字。

 

2.2 函數

 

  函數是業務邏輯的載體。大部分時候,我們都是在函數中進進出出、上上下下、左左右右。這裏涉及到了函數之間的跳轉、函數內的導航。對於函數的編寫,同樣有着一些應該遵守的最佳實踐。

 

    1. 短小。函數的第一個規則就是要短小。短小才精悍,濃縮的都是精華。函數短小的好處:
      1. 對於大部分大腦不是特別發達的人,10個10行的函數可能比一個100行的函數理解起來更容易。如果給每個“小函數”能起一個具有說明性的名字,那麼整個流程讀下來會更清晰,而且可以增加代碼自解釋的作用。我曾經重構過一個接近1000行的函數,那絕對是噩夢。
      2. 短小的函數便於在電腦屏中完整的顯示出來,不需要上下去滾動屏幕。
    2. 只做一件事。函數應該做好一件事,做好這件事,只做這件事。這個原則(單一職責原則)表述起來很簡單,但實際執行過程中卻困難重重。難點就在於怎麼去區分“事”。你覺得“吃飯”是一件事嗎?是的,它可以只是一件事。但是如果你要在更低的層次去細分,你還可以將“吃飯”拆分成“拿碗 -> 打飯 -> 吃飯 -> 洗碗”這些過程。當然,這個例子比較牽強,但還是可以說明問題的。按不同的粒度可以劃分出不同的結果。那到底應該將一件事“細分到多細”呢?需要找到一個平衡。當我們將一件事劃分的越細、粒度越小,那麼它的靈活性就越高,複雜度也越高。反之,則靈活性降低、易用性提高。比如,“棉花”就有較高的靈活性,我們可以用它來製作“棉被、被套、布匹”等,相應的它的複雜度也較高,大部分非專業技能人士應該都不可能將一堆“棉花”加工成一塊“布匹”。與“棉花”相比,“布匹”的層次高一些,高層次也導致了它靈活性的降低,“棉花”可以加工成“棉被”,而“布匹”則不應加工成“棉被”了,也就說“布匹”的可能性相比“棉花”更少。此外,從“布匹”加工成一件“衣服”的複雜度比起從“棉花”到一件“衣服”的複雜度有了極大的降低。
    3. 每個函數一個抽象層級。讓代碼擁有自頂向下的閱讀順序,讓每個函數後面跟着位於下一抽象層級的函數。也就是說,將類中的函數按照抽象層次的順序放置在文件中,這樣的好處在於可以順暢的從上到下的閱讀整個類。函數的順序應該像小說一樣被精心安排。
      1. 函數中混雜不同抽象層級,往往讓人迷惑。
      2. 如果和一羣你的“領導”(抽象層級高)的人一起吃飯,你會“不自在”。
      3. 如果和一羣你的“下屬”(抽象層級低)的人一起吃飯,你會咋樣呢?哪個領導來表達一下此種情況的心情::-)
    4. switch語句。有需要的時候可以利用多態來優化 switch 語句。
    5. 使用描述性的名稱。與之前在命名中介紹的一樣,我們需要使用具有描述性的名字。不要害怕長名稱。
      1. 使用描述性的名稱能幫助理清模塊的設計思路,並幫助改進它。
      2. 命名的方式要保持一致。
      3. 函數越短小、功能越集中,就越便於取名字。在函數名字中存在  and  時一般體現來了該函數的職責不單一。
    6. 函數參數。
      1. 函數參數越少越好。
      2. 從測試角度來看,參數越多,就越難寫出能確保各種參數組合正常運行的測試用例。
      3. 輸出參數比輸入參數難以理解。習慣認爲信息通過輸入參數傳入函數,通過返回值從函數中輸出。不太希望信息通過輸入參數傳出。輸出參數,就是將一個對象通過參數形式傳入一個函數,然後在函數中對該對象的值進行處理,在該函數外部可以通過該對象的引用使用函數處理後的值,達到函數輸出的效果。
      4. 標識參數醜陋不堪。不要向函數中傳入布爾值。
    7. 無副作用。
    8. 分割指令與詢問。函數要麼做什麼事,要麼回答什麼事。
      1. 函數應該修改某對象的狀態(指令),或是返回該對象有關的信息(詢問),兩者都幹常會導致混亂。
    9. 使用異常替代返回錯誤碼。使用Java的異常系統來處理錯誤,而不是通過自定義的錯誤碼來處理各種異常的情況。
      1.   try / catch 代碼塊搞亂了代碼結構,把錯誤流程和正常流程混爲一談。最好把  try  和  catch  代碼塊的主體部分抽離出來,形成函數。
      2. 使用錯誤碼容易形成依賴磁鐵 dependency magnet,依賴磁鐵類會被很多類導入和使用。當這個類修改時,所有其他依賴它的類都需要重新編譯和部署。
    10. 別重複自己DRY。
      1. 重複是軟件中一切邪惡的根源。許多原則與實踐都是爲了控制與消除重複。
      2. 存在不同層次的消除重複。可以將一段語句塊提取爲一個函數來消除重複,可以將一些函數提取到父類來消除重複,可以將一個模塊提取爲公共服務來消除重複,就像開源屆流行的說法:不要重複製造車輪。

 

  藉助於高級開發工具的幫助,我們可以方便的在函數、類之間跳轉,可以方便在函數中添加任意的參數,但是如果拋開工具的輔助,那麼曾經沒有在意的“細節”就會形成難以清除的“污垢”,讓人難受。當然,我們完全沒有必要拋棄高級的開發工具,但是如果能在日常工作中就注意並保持代碼中的每一個細微之處的整潔,難道不是一件很有“工匠精神”的事情嗎?

  大師級程序員把系統當作故事來講,而不是當作程序來寫。

 

2.3 註釋

 

  註釋就是函數、類的一種輔助說明,就是怕別人不懂自己寫的代碼的意圖,就加上一段說明性的文字。我經歷過兩個極端:一個是完全不寫註釋,另一個是每個函數、類都需要寫上註釋。

  我個人更認同的觀點是:如果我們擅長於用語言來表達意圖,那麼就不需要註釋。

  關於註釋特別要注意的一點是:當代碼在變動的時候,註釋並不總是跟着變動的。這樣會導致註釋常常會與它所描述的代碼不同步,並越來越不準確,甚至可能會起誤導的作用。

 

  1. 註釋不能美化糟糕的代碼。少量而準確是註釋比大量而毫無意義的註釋更有用。大量無用的註釋會打亂閱讀代碼時的思路,也會增加滾動屏幕的成本。
  2. 用代碼來闡述。儘量使用代碼來闡述它的意圖。這需要給函數、參數起恰當的名字,函數層次清晰、邏輯明確。
  3. 好註釋。
    1. 法律信息。根據公司要求而定。
    2. 提供信息的註釋。
    3. 對意圖的解釋。註釋可以提供某個決定背後的故事。
    4. 闡釋。註釋可以解釋某些難懂的參數或返回值的意義。
    5. 警示。
    6. TODO註釋。TODO大多都是程序員的自我安慰、自欺欺人。
    7. 放大。可以用來放大某種不合理之物的重要性。
    8. 公共API中的Javadoc
  4. 壞註釋。大多數註釋都屬於此列。
    1. 喃喃自語。如果決定寫註釋,那就花點時間確保寫出最好的註釋。
    2. 多餘的註釋。典型的就是對Bean類中的每個參數都寫上註釋,比如 name, length 等可以通過名字就判斷意義的字段。
    3. 誤導性的註釋。這個最可怕。
    4. 循規蹈矩式的註釋。很多IDE都會自動生成函數的註釋,其中可能會包含 @param, @return 等信息的說明,大多數時候可能就是放了一個IDE生成的模板在那裏,並沒有任何實質性的內容。
    5. 日誌式註釋。在每次修改時,在模塊開始處添加一條註釋,註明此次的修改變動。這個工作應該是Git等版本控制系統該做的事。
    6. 廢話註釋。毫無意義的註釋。
    7. 能用函數或變量時就別用註釋。體現了要使名稱更具表達性,能表達它自身的意圖。
    8. 位置標記。#######################################之類的。
    9. 括號後面的註釋。通過註釋來表明這個括號與哪個括號是一對的。
    10. 歸屬與署名。同樣應該是代碼控制系統應該做的事。
    11. 註釋掉的代碼。
    12. 非本地信息。
    13. 信息過多。
    14. 不明確的聯繫。

 

  註釋應該起着輔助的作用,而不應該“喧賓奪主”。好的註釋在於提供有用信息、或者方便程序員獲取有用信息。而壞的信息在於冗餘,甚至是錯誤,一般它們對提高對系統的認識不會提供任何幫助,反而會分散注意力。

 

2.4 格式

 

  好的團隊,應該有一份屬於團隊的編碼規範。這裏面需要定義代碼的格式問題,目的是爲了保證團隊的人輸出的代碼就像是一個人寫的。這樣的好處,1)在於減少團隊間相互適應不同格式的成本,2)在於提高團隊對外輸出的影響力。

  代碼的格式關乎溝通。下面是一些格式的最佳實踐:

  1. 垂直格式。
    1. 向報紙學習。源文件的頂部應該給出高層次概念和算法,細節應該往下逐次展開。
    2. 概念間垂直方向上的區隔。每個空白行都應該是一條線索,標識出新的獨立概念。
    3. 垂直方向上的靠近。相互靠近的代碼應該是緊密相關的代碼。
    4. 垂直距離。
      1. 變量聲明。應該儘可能的靠近其使用的位置。
      2. 實體變量。應該在類的頂部聲明。
      3. 相關函數。調用者應該儘可能的放在被調用者的上面。
      4. 概念相關。概念間的相關性越強,彼此之間的距離就應該越短。
  2. 水平格式。
    1. 水平方向上的區隔與靠近。
      1. 在操作符周圍加上空格字符,可以達到強調的目的。
      2. 不在函數名和左圓括號之間家空格。
      3. 函數參數之間用空格隔開。
    2. 縮進。縮進有助於理清代碼的層次結構。  
        // 風格1
        if (condition) {
            statement;
        }

        // 風格2
        if (condition)
        {
            statement;
        }

  關於下面的兩個 if 語句塊,你是哪種風格?我是風格1,曾經被人當面說風格1怎麼怎麼不好,應該使用風格2,什麼什麼的!氣氛一度十分尷尬。
  其實,這也沒什麼大驚小怪的,每個程序員都會有自己喜歡的風格。但是,在團隊中,那就應該形成一致的團隊風格。

3. 總結

  

  上面介紹了關於命名、函數、註釋、格式相關的一些概念性知識點,代碼整潔之道不是銀彈,它只是幫助我們做好代碼層面的小事。如果能夠堅持下去,那今天的付出可能就是一隻蝴蝶開始扇動了翅膀,可能在未來的某一天帶來意想不到的巨大收貨。拋開這些功利主義的想法,純粹的想要成爲一個有着“工匠精神”的程序員也是一件很牛逼的事吧!

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