程序中註釋的種類及替代方案

http://kidneyball.iteye.com/blog/1735698

在 To 註釋 or not 註釋, that is a question 中,我認爲程序中的內部註釋——如果百分百準確地話——雖然在一定程度上對閱讀程序有幫助,但在變化的項目中,卻引入了註釋自身的維護問題。而註釋如果缺乏維護,最終將形成失效或者誤導性的干擾信息,反而對妨礙了程序的正常維護。而註釋的維護,是很難開展的,既缺乏有效的管理和技術手段,又缺乏明確的責任界定。因此建議是:寫程序的人,儘量少寫內部註釋。讀程序的人,儘量少依賴內部註釋。那麼,問題來了,在當今的技術環境中,有沒有可以替代註釋的技術方案,既能使程序容易閱讀,又沒有內部註釋的種種問題? 

好消息是,不但有,而且都是已經在開發行業中使用了多年的成熟的方案。壞消息是,如果沒有使用它們的習慣,在剛開始時不如隨意寫個註釋順手。所以問題只是我們肯不肯用。首先面臨的問題就是,針對不同的註釋,替代方案是不同的,我們需要針對不同的情況來選擇。那麼下面就來談一談內部註釋的不同種類,以及它們的替代方案。

在開始之前,先強調一下: 

1. 全文都是個人觀點,就不在每一句之前都加上“個人認爲”了; 

2. 這些替代方案均需要開發工具支持,例如IDE,版本控制工具等等。本文主要針對在協作環境中開發業務系統的一般情況。在遠程終端裏用vim寫程序的朋友請完全跳過本文。 

3. 如非特別說明,文中“註釋”表示程序的內部註釋,不包括在公共方法或類上以註釋形式編寫,實質是聯機文檔的文字,例如JavaDoc; 

4. 在內部註釋中,對沒有任何代碼與之對應的註釋(比如說,TODO和FIXME)不在討論範圍內。詳情請參看前文,我認爲這些註釋的維護難度與危害性都不如說明具體代碼的註釋嚴重,而且尚沒有好的替代方案,在這些情況下應該繼續使用註釋; 

5. 下文介紹的替代方案和工具的選擇都只是我自己常用的,不排除有更好的。就當拋磚引玉吧。 

好,下面開始。 

第一種註釋:提示性的行內註釋 

這種註釋產生的原因是,在編寫或閱讀程序時,程序員感覺到在大段代碼中有一個代碼片段不好理解,於是在這段代碼之前或行末加入註釋說明。這類註釋通常是針對這段代碼稍微高層含義的簡短說明。比如說,在一段雙重循環前面註釋:“冒泡排序”或者“按照員工年齡排序”等等 

這種註釋最大好處是,在排查錯誤時它能幫助程序員跳過大段的無關片段,但一旦失效,危害也很大。比如說“按照員工年齡排序”的循環片段,有可能其實是“按照員工工齡排序”(錯別字),也很難避免在後續維護中有人順便在這個循環裏乾點什麼其他事情,而又沒有修改註釋。從而導致你跳過了發現問題的關鍵位置。相對來說,這種註釋對閱讀代碼的負面影響就不太大,它的好處在於能幫助我們快速瞭解一段代碼的設計意圖,在閱讀時不會太迷惘,即使失效了,因爲最終還是要閱讀代碼,所以除了浪費時間,一般不會有大的問題。 

順便扯一下,對於一個新手,這種註釋確實對理解代碼有幫助。但最好不要忘記,從代碼直接推導設計意圖也是程序員的基本功之一。如果你發現離開了註釋就對一些複雜的代碼完全無從入手,或者容易犯困或頭痛,那最好先把這種基本功鍛鍊起來再借助註釋提高速度。有一個事實是無法改變的:無論你拿到的代碼有沒有註釋,活總是要乾的,而你總有一天會遇到沒有註釋的代碼。如果你根本不能完成的事情,而別人努力一把就能完成,即使你把那個寫程序不寫註釋的人上下三代都罵遍,你也無法改變在經理眼中你的能力就是比別人差的印象。 

言歸正傳,在實踐中如何消除這種註釋呢,主要手段就是用命名代替註釋。對於變量或常量,與其隨便起個名字,然後用註釋來說明它是幹什麼的,不如直接讓它的名稱說明自己是什麼(英文不好?這個下面會談)。 

至於複雜的表達式和程序片段,既然你能一段提示性的註釋說明它的功能,這說明它是一個相對獨立的功能單位,完全可以把它作爲一個獨立的方法,然後爲這個方法取一個合適的名字來表達自身。如果這個方法有可能會被子類或其他類使用,可見性定爲protected或public時,就應該在這個方法上加上文檔性註釋,詳細說明功能以及用法。但私有方法加文檔性註釋要慎重,私有方法上的文檔缺乏交叉檢查,職責不清,程序員們不會重視私有方法註釋的維護,失效的風險仍然頗大,不過總比在程序內部的註釋要好上不少。 

作爲替代方案,當然是最好能保持本來的工作流程。這種提示性的註釋,往往是你寫了一段代碼之後,發現較難理解又回頭補上的。也可能是重新閱讀代碼時補上的。總之,是先有代碼片段,再寫相應的註釋。在替代方案中,你自然也希望能先寫代碼,再爲它命名。那麼,當前大部分IDE環境所帶的“重構”->“抽取”功能就完全符合這個要求了。 

常用的“抽取”功能有三種,抽取常量,局部變量和方法。下面分別介紹一下它們在eclipse和在intellij中的操作方式。 

抽取常量 

在eclipse中抽取常量方式一 

 

補充說明: 
1. 第一步中,先把光標放在需要抽取的表達式內部任意位置,按“alt+shift+上箭頭”選中上一級語法結構,直到你需要抽取的部分被完全選中。(當然你也可以用任意其他方式選中需要抽取的表達式,這裏只列出我自己覺得比較方便的慣用法供參考,下同)。 

2. 選擇Extract to constant後出現的代碼模板中,有多個輸入點可供選中,例如可把private改爲public,改變常量的類型或名稱等,通過TAB鍵切換。 

在eclipse中抽取常量方式二 

 

補充說明: 
1. 調出重構菜單後,直接按A鍵即可選擇Extract Constant 

2. 在彈出的對話框中,如果選中Replace all occurrences of the selected expression with references to the constant (默認爲選中),則本文件內所有相同的表達式字面量均被替換爲引用這個常量。 

在Intellij中抽取常量 



補充說明: 
1. 在彈出的氣泡框中,選中Replace all occurrence將替換文件內所有相同的表達式字面量 

2. 氣泡框中的選擇項設定一次後,下次同樣操作將使用前一次的選擇。 

3. 在氣泡框出現時再按一次Ctrl+Alt+C將出現詳細對話框,在這個對話框中可選擇同時將抽取出來的常量繼續抽取到另一個類上去,當項目中使用一個專門的常量類集中放置常量時可使用該選項。 

抽取局部變量 

在eclipse中抽取局部變量方式一 

 

補充說明: 
1. Ctrl + 1彈出的Quick Fix選項中關於抽取局部變量的選項有兩個,其中一個將替換當前方法內所有相同的表達式字面量。(後面關於replace all occurrence就不再重複解釋了) 

在eclipse中抽取局部變量方式二 

 

補充說明: 
1. 勾選“Declare the local variable as "final"選項後,可自動將創建的變量聲明爲final。一般情況下,個人建議勾選此選項,等確認變量需要修改時再去掉。這樣可以提示閱讀程序的人那些變量在後續執行中會發生改動。 

在Intellij中抽取局部變量 

 

抽取方法 

在eclipse中抽取方法方式一 

 

補充說明: 

1. 重構工具會自動推演新方法所需的參數和返回值類型 

在eclipse中抽取方法方式二 

 

補充說明: 

1. 在彈出的對話框中可以對方法的可見性,參數等進行調整。 

在Intellij中抽取方法 

在Intellij中可以用Ctrl+W (功能與eclipse的Shift+Alt+上箭頭相似)選中需要抽取爲方法的表達式或程序片段,然後用Ctrl+Alt+M開啓彈出對話窗,與上面的eclipse方式二類似,在此就不重複了。 

============================================================================= 

可以看到,抽取功能除去命名外,每個動作的按鍵次數在兩次到五次左右,基本上不會對正常的開發和閱讀流程產生影響。.最後我們來看看兩種代碼的對比 

註釋方式: 

 

重構爲自描述代碼的方式 

 

對比一下,自描述的代碼雖然略長,但在保持可讀性的前提下,消除了兩個“神祕量” ("save" 和 args[0] ),後續代碼如果需要可以直接引用抽取出來的常量或變量 (很可能,特別是args[0])。這樣無形中提供了一個統一的修改點,例如說以後需求變化,command參數的位置發生了變動,很容易就能統一改過來。 

至於抽取出來的shouldExecuteSave方法,我們也很容易發現這樣一來,將來很容易就會出現一堆的shouldExecuteXXX方法。在前面的基礎上,保持可讀性不變的前提下,可以很自然地想到將它改爲: 

 

這樣,後續的判斷都可以統一使用這個方法,形成了又一個統一修改點。並且這個方法可以單獨進行單元測試。可以看出,把程序重構爲自描述方式,在保證可讀性的前提下,不但免除了需要額外維護註釋的麻煩,還提供了額外的可擴展性,可測試性,一舉四得。 

如果資深員工主動帶頭進行這種重構,能輕易把這種風格推廣開去。不妨想象一下,將這兩段代碼分別交給新人維護,拿到第一段代碼的人必然是把這個片段直接複製,然後修改“save”字符串字面量和註釋(如果他還記得的話),導致程序中到處都是直接引用字面量和結構相似的表達式。而拿到第二段代碼的人,也會自然地跟隨現有的代碼風格,創建新的字符串常量,複用已有的方法。 

你也許會懷疑,爲了搞什麼“自描述重構”去背那麼多快捷鍵,改變自己的開發習慣是否值得。好消息是,關於這一點完全不用糾結,即使你不是有意識地進行“自描述重構”,在日常定義常量變量和方法時就充分利用抽取功能也能顯著提高你的開發效率。從前面的操作示例已經可以看出: 

抽取常量可以幫你省去輸入數個修飾符(private static final String)的工作,並且避免了自行在當前輸入位置和常量代碼段來回跳轉的麻煩; 

抽取局部變量可以幫你省去一次輸入變量類型以及final關鍵字的麻煩,對於一些名稱具有明確業務含義的類,特別是單例類,可以免除輸入變量名的麻煩。IDE會自動從類名推演出一些候選變量名稱供選擇; 

抽取方法可以幫你省去輸入返回值,參數列表的麻煩。對於一些臨時起意要創建的短小方法,先輸入實現再抽取爲方法可以避免思路中斷。 

第二種註釋:對方法的實現邏輯作框架性註釋 

寫這種註釋的意圖是,在編寫方法時,先不動手直接寫代碼。而是把設計思路和邏輯框架用註釋的形式先列好,經過檢查和評審確定思路正確後,再往這些已經列好的註釋中間填上實現代碼。 

這種做法由來已久,在1993年出版的《代碼大全》(《Code Complete》)中,在第四章《建立子程序的步驟》就提到了一種稱爲PDL的程序設計語言(Program Design Language) ,應該算是對這種開發模式較爲成熟的總結了。一個使用這種方式來設計的框架性註釋可能是這樣的: 

Java代碼  收藏代碼
  1. private boolean createDialogResource() {  
  2.     //檢查已在使用的資源數量  
  3.     //如果有其他資源可用  
  4.         //嘗試爲一個對話框分配資源  
  5.         //如果資源分配成功  
  6.             //登記該資源已被佔用  
  7.             //對該資源進行初始化  
  8.             //將資源號寫入由調用者指定的位置  
  9.         //EndIf  
  10.     //EndIf  
  11.     //若新資源創建成功,返回true;否則返回false。  
  12. }  


理想狀態是,人們會上面的這種設計思路進行討論和評審,確定下來後,再在每行之間填入實現代碼,這樣原本的設計草稿就直接變成了代碼註釋。 

按照《代碼大全》的總結,這種做法所帶來的好處有 

引用

儘管第二段 PDL 是完全用自然語言寫成的,但它卻是非常詳細和精確的,很容易作爲用程 
序語言編碼的基礎。如果把這段 PDL 轉爲註釋段,那它則可以非常明瞭地解釋代碼的意圖。 
以下是你使用這種風格的 PDL 可以獲得的益處: 

1 PDL 可以使評審工作變得更容易。不必檢查源代碼就可以評審詳細設計。它可以使詳 
細評審變得很容易,並且減少了評審代碼本身的工作。 

2 PDL 可以幫助實現逐步細化的思想。從結構設計工作開始,再把結構設計細化爲 PDL, 
最後把 PDL 細化爲源代碼。這種逐步細化的方法,可以在每次細化之前都檢查設計, 
從而可以在每個層次上都可以發現當前層次的錯誤,從而避免影響下一層次的工作。 

3 PDL 使得變動工作變得很容易。幾行 PDL 改起來要比一整頁代碼容易得多。你是願意 
在藍圖上改一條線還是在房屋中拆掉一堵牆?在軟件開發中差異可能不是這樣明顯, 
但是,在產品最容易改動的階段進行修改,這條原則是相同的。項目成功的關鍵就是 
在投資最少時找出錯誤,以降低改錯成本。而在 PDL 階段的投資就比進行完編碼、測 
試、調試的階段要低得多,所以儘早發現錯誤是很明智的。 

4 PDL 極大地減少了註釋工作量。在典型的編碼流程中,先寫好代碼,然後再加註釋。 
而在 PDL 到代碼的編碼流程中,PDL 本身就是註釋,而我們知道,從代碼到註釋的花 
費要比從註釋到代碼高得多。 

5 PDL 比其它形式的設計文件容易維護。如果使用其它方式,設計與編碼是分隔的,假 
如其中一個有變化,那麼兩者就毫不相關了。在從 PDL 到代碼的流程中,PDL 語句則 
是代碼的註釋,只要直接維護註釋,那麼關於設計的 PDL 文件就是精確的。 


我曾經是這種開發模式的忠實實踐者——當我還是學生的時候。但在實際參與項目之後,就發現情況並沒有想象中理想。在普通的業務系統項目中,根本就沒有人會給你評審PDL,基本上不可能你爲每一個方法寫好PDL,就有幾個人等在那裏給你檢查評審。而你也不可能寫好PDL之後就坐在那裏等着有人評審認可後再開工。所以,事實上PDL在大多數情況下就是你自己寫給自己看的草稿,這樣上述1,3點就根本沒用了。其次,我參與實際項目時,面向對象已經開始流行(93年時我還在用BASICA和PASCAL,大概是98年看到這部書時,頗實踐了一下,同時也開始慢慢接受DELPHI的面向對象方法),UML成爲了細化設計的主要工具,所以第2點也過期了。 

至於第5點,就是我們之前一直在說的,如果是爲了不惜代價保證文檔(註釋)與代碼同步,那維護註釋當然比維護分離的設計文檔要方便。但維護註釋這種事在實際中就很難管理和推行。而一旦產生不同步,過期的註釋比過期的文檔破壞力大得多。因爲我們都知道設計文檔通常情況下都與代碼不同步,只能反映出大致的設計思路。而對於註釋我們總是假定它與代碼同步的(上一篇文章已經談過,如果你假定註釋與代碼不同步,那註釋就根本沒有任何作用)。況且,從閱讀體驗和表現手段角度來看,圖文並茂的文檔實在比行內註釋實在強太多。 

所以最後,就只剩下第4點。換句話說,今時今日,我們使用這種結構性註釋的最大作用,就是方便我們寫註釋。 

言歸正傳,在目前的技術條件下,有什麼辦法取代這一種註釋方式呢?答案很簡單,把這每一條註釋都以調用方法的方式在代碼中體現出來。例如前面提到的createDialogResource方法可以這樣寫: 

 

注意這時很多方法甚至某些類都還未創建,參數表也還未確定。但是如果忽略中英文的差別,這段代碼所表達的意思與使用PDL註釋的方式是幾乎一致的。所不同的是,接下來我們不是要在逐行之間填入代碼,而是需要分別創建和實現這些方法和類。 

使用IDE的Quick Fix功能,我們可以很方便地從調用位置反向創建出對應的方法來。 

 

由於IDE在反向創建方法時會自動推演參數表,因此我們可以在開始創建某個方法之前把預計會用到的參數寫入調用位置再使用反向創建功能。 

比較兩種方式,後者在保證了可讀性的同時,消除了維護註釋的煩惱,並且把一個長方法拆分爲數個功能相對獨立的短小方法。這些短小的方法具有明確的輸入輸出,避免了一些內部變量的交叉混用。它們可以獨立測試。它們可以被複用從而避免了直接複製代碼片段。 

第三種註釋:關聯業務需求 

所謂關聯業務需求的註釋,就是在某些需要特殊處理或修改過的地方,用註釋的方式指明這個處理是誰誰誰,某年月日,針對某個bug或需求所做的處理,註釋中往往帶着bug跟蹤系統的問題編號,或者需求文檔的章節號。 

一般來說,如果這種處理與上下文的邏輯結合得非常自然,這個需求或修正也非常符合人類思維習慣,那麼是不需要做這種特別說明的。除非團隊裏有個人無聊得喜歡把代碼和需求文檔一行行的對上,但這樣的話維護註釋將是個噩夢,而且過多的噪音將掩蓋真正有用的信息。寫這種註釋的程序員當時的想法往往是: 

引用

1. “這不是一種常規做法,我覺得仍可改進,不過這裏有一個古怪的需求(或bug),在你打算做任何改進之前,需要注意不要違反了這項需求(或重現這個bug) 

2. “我在做這個處理時時間很緊,只進行了簡單的測試,不知道會不會引入其他錯誤。如果後來出現了錯誤,而你定位到這裏的話,請注意不要因爲改正那個錯誤而違反了這項需求(或重現這個bug) 

3. “我也覺得這段代碼與附近的代碼格格不入,放在這裏很詭異,不過我沒有時間去找到更適合的位置了。如果你看到時覺得很礙眼很醜陋,這個需求(或bug)就是原因,請不要來找我了” 

4. “寫給未來的自己:如果你覺得這段代碼很醜陋,想跳起來在項目組裏公開罵娘,請先看看這段註釋上的署名” 

5. “我在其他地方看到過這種註釋,我覺得很酷,所以我在維護代碼時做任何修改都會加上這樣的註釋” 


如果是屬於前4種,我覺得這樣的註釋確實應該出現在代碼中(符合我在上一篇中提到的非常規處理方式的說明註釋)。但是如果是第5種,我的建議是最好停止這種行爲。這樣幹最大的害處是,大量這種無用信息將掩蓋前4種真正有用的信息。須知道在閱讀代碼或排查錯誤的過程中不停跳去查閱某段文檔或閱讀某個bug信息是一樣很中斷思路的事。如果是一些通過閱讀代碼就能明確的事情,經常由於看到這樣的註釋而去翻查文檔或bug信息,到最後又沒有任何實質性的幫助,長此以往程序員就會對這類信息選擇性失明。這包括濫寫註釋的人自己,甚至極有可能是專門針對自己寫的註釋選擇性失明。 

而對於維護代碼的人來說,前四種也有兩個很明顯的缺點: 

首先,寫這種註釋純粹是個人行爲,最常見的動機是別人問起時能有個交代,聰明人固然會寫,但即使不寫,也無從監督。但缺失這類信息是很麻煩的,經常導致兩個bug輪流出現,改好A,B就來了;隔一段時間發現B,改好了A又回來了。 

其次,時間緊迫的特殊處理往往來不及做良好的設計,而涉及多個類或文件互相以某種“隱含約定”的方式互相配合。最常見的情況就是在Java類裏往某個Map或Context裏放入一個值,而在頁面上則直接通過鍵值的字面量(通常是字符串)直接取得該值。過一段時間後,如果頁面結果發現出錯,維護的人很難追查到這個值是由哪個Java類put進去的。這種情況,即使你在兩個文件上分別作了類似的註釋,也於事無補,因爲它們之間沒有引用關係,看似毫無聯繫。最好的解決辦法是是需要查出與某個修改位置被同時修改的有哪些文件。 

那麼從團隊的角度出發,有什麼更好方案呢。 

出於前4種動機的需求關聯註釋還是應該寫,它們能起到一個很好的提醒作用。但是作爲維護代碼的人,不能只看這種註釋,在任何你覺得奇怪,醜陋,低級,但在某種情況下又能正確運行的代碼,在修改前都要先搞清楚這段代碼的修改歷史和修改動機。而找到這類信息的最好地方,就是版本控制系統的提交歷史記錄。 

爲了達到這個目的,建議團隊管理者能做到以下幾點: 

1. 要求每個程序員認真填寫提交記錄,要寫清楚修改的原始需求編號或問題跟蹤編號。簡單說明本次提交本次提交解決了什麼問題或引入了什麼功能。以及有哪些需要注意的地方。禁止使用一兩句無實質意義的提交記錄(例如:“問題修正”,“提交代碼”之類) 

2. 提倡“小提交”,每解決一個功能點,修正一個bug,只要能編譯通過,就應該提交。如果同時解決了多個問題,則應該分開提交。最終目標是,每次提交的提交記錄都說明一個單獨的功能點,而所提交的文件都與該說明相關。

3. 瞭解團隊中所使用的版本控制工具的功能與侷限。比如說,CVS不能容易獲取到同一次提交的有哪些文件,而SVN或GIT都能很容易辦到。SVN在文件改名或移動後無法跟蹤其歷史記錄(因此除非必要,不要隨意對一些有長期維護歷史的文件進行移動、改名、刪除重建),GIT則能辦到,等等。 

以上幾點都是可監督,可跟蹤的具體要求,比起“寫好註釋並且保證與代碼同步”應該更容易落實。 

如果團隊能堅持這幾點,那麼程序員就可以通過版本控制工具的Annotation (又或者叫blame)功能來查看文件中每一行所對應的業務功能,以及修改人和修改日期。 

舉例來說,在Intellij中打開右鍵菜單,選擇Git -> Annotate (假設你使用了Git作爲版本控制工具) 

 

就能在編輯器左側顯示對應到每行的最後更改記錄: 



補充說明: 
1. 在左側的Annotate區中一共四列,分別爲:提交編號、提交日期、提交用戶、提交序號。最後一次更改的行會加粗顯示並在右側加星號。在這個例子中,所顯示的文件從創建開始一共被提交(更改)了8次(包括創建那一次)。提交序號爲8的就是最後被更改的行,序號爲1的行則從創建之後就一直未被更改過。 

2. 鼠標移到某行的Annotate上即可出現該提交的詳細提示,包括提交記錄。 

3. 由於我臨時下載的eclipse上沒有裝插件,就不截圖了。我記憶中在文件上打開右鍵菜單,選team -> Show Annotation 就可以打開Annotation。不過沒有Intellij這麼明顯,而是用一些小色塊來表示提交,鼠標懸浮時會彈出氣泡提示顯示提交信息。 

這種細緻到行的提交記錄能提供比行內註釋更詳盡和準確的業務需求信息。並且,雙擊某一次提交的Annotation,將列出這次提交所涉及的所有文件,解決了前面所說的無法獲得某個修改所涉及的其他文件的問題。 

 

第五種註釋:中文註釋 

在上一篇文章提到的討論貼裏,有一種觀點頗有趣,估計也很普遍:寫大量註釋,是因爲我或我團隊裏的其他人看不懂英文,而且命名時使用金山詞霸不準確或太麻煩。 

我對此這種論調的看法是,作爲一個開發者,只要稍微有點抱負,那要麼就鍛煉出不依靠註釋看懂程序的能力,要麼懂英文,二者至少要選一。如果既看不懂程序又看不懂英文,那就意味着對於任何由非中文母語程序員寫的程序,他只能停留在看着翻譯文檔使用的層面。那麼在能預期的至少20年內,他在開發技術方面要真正有所建樹將非常困難。困難程度絕對超過他用金山詞霸學一些計算機和特定行業英文。 

對編寫自描述的程序來說,需要的所有英文技能就是用英文來表述一個名詞或動詞,什麼修辭語法時態文化全部不用理。隨便一套電子辭典都絕對足夠有餘。 

況且,如果是因爲英文不好而不能爲變量或方法正確命名的話,無論你在定義的位置註釋得多麼詳盡漂亮。在所有引用的位置照樣看得人一頭霧水。 

因此我個人的建議是:目前正好處於既看不懂沒有註釋的程序又看不懂英文狀態的朋友,如果完全沒有興趣去改變的話,那麼最好儘快開始考慮程序員之後的下一份職業了,或者學一門使用中文做標識符的語言然後自己創業。
發佈了136 篇原創文章 · 獲贊 26 · 訪問量 69萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章