代碼質量隨想錄(二)必也正名乎

  不必被我的標題嚇到哈,孔老夫子時代沒有電腦。如果有,估計諸子百家們還得針對軟件工程抒發一系列代碼質量倫理學的教條。

  上回文章說到,代碼品質改進應該在三個層面上展開,其中最微觀的就是代碼段的質量考究了。很多時候我在針對一些項目做工程分析和大規模重構之前,首先希望對大概的工作原理有些瞭解,這個時候就要深入核心模塊的文件之中,挑選代碼來閱讀,以求理順思路了。根據個人的經驗來說,微觀的改進往往能夠激發大規模的結構重組。所以一連幾篇文章,分別會談到“好名稱”、“好格式”、“好註釋”三個微觀的表層質量改進問題。

  深入到函數或方法內部的代碼之後,就要面對一行行具體的代碼了。此時最應該關注的首先就是標識符的命名問題。這個問題基本上是講重構或代碼質量的書所必談的話題之一。記得馬叔叔曾經在《Clean Code》中說,給標識符起名時,應該像給你們家小朋友起名字一樣認真(大意,並非原文)。當時我看到此話不禁微笑了一下。是哇,很多時候我在代碼評審中遇到的思維不順都是源於名字問題。

  一直以來,朋友和同事都偶爾會拿整個項目或是代碼片段來和我討論,對於企業級開發領域,我看的代碼不多,對代碼質量不便妄言,不過具體到和我關係比較密切的移動開發領域,可就真的是令我非常頭疼了。由於移動軟件或遊戲的開發經常週期很短,而且重結果,輕過程,更不講求後續的版本更新、維護與複用。所以經常在開發過程中程序員容易在工期的壓力下過於隨心所欲,導致項目的代碼理解起來大費周折。有時候我越是急於理解,就越是摸不着頭緒。後來想想,很多困難都源於具體的標識符名稱。必須理解了它們,纔有可能理解更高層級的內容。

  通過閱讀《The Art of Readable Code》以及其他相關的書,我漸漸把原來學到的一些代碼質量知識總結起來了。ARC這本書的好處之一就是,它講的東西不見得多新,很多都是Clean Code或者類似的書中講了又講的話題,不過,它善於把這些零散的知識點按照一定的框架整合起來,讓我能夠更系統地歸納並鞏固這些知識。

  簡單的說,好的標識符名稱,必須封裝恰當的信息,同時不致誤解。

  至於如何封裝恰當的信息,這個問題要看個人的把握,有幾條能夠作爲指導的建議,不妨梳理給大家來看。

1.選擇更具表達力詞語

  我自己在代碼中就經常忽視這一點,用慣了get和size之後,遇到什麼情況,不管具體細節,一律使用getXXX或size作爲方法名稱。今天就看到了幾個反例。例如

  1. class BinaryTree{ 
  2.     public int size(){...}   
  3. }  

  這個size到底獲取的是高度,節點數還是佔據的內存字節數?這三種情況應該分別用更爲特定的height、nodeCount或occupiedMemoryBytes來表示,而不是空泛的size。

  說到這個問題,我覺得增加個人的詞彙量是非常有好處的。可以經常翻看英英詞典來瞭解各個詞語之間的細微差別。例如用“deliver, dispatch, announce, distribute, route”(投遞、派發、播報、分配、按指定線路發送,就是路由)之中的某個詞代替send(送),用“search, extract, locate, recover”(搜索、提取、定位、重新找回)代替find(找)等等。

  有一個問題,就是命名含義豐富了會不會影響以後的修改。有同學可能會說,我故意放一個朦朧且曖昧的size來代替height、nodeCount或occupiedMemoryBytes,這樣將來萬一內部的邏輯有變化,我直接修改具體代碼就行了,連size這個方法名都不用修改,豈不是更符合“針對接口而非實現來編程”的面向對象設計理論麼?一開始我也有這個想法,後來想想後果十分可怕,這樣做根本就沒有明確表述出該接口的具體意圖:一旦將表示height的size方法之中的算法改爲返回nodeCount,而保留size方法名不做修改,那麼這會害苦了該API的客戶代碼編寫者們。你的同事仍然以爲size返回的是二叉樹的高度,殊不知現在它返回的是節點數目了。一旦出現這樣的bug,除非兩人緊密配合,否則調試很費時,而且隨着時間的推移更爲難辦。反之如果方法名從height改爲nodeCount,那麼下游開發者在源碼管理系統中更新代碼時立刻就看出其中的差別,從而能夠很從容地修改已有的邏輯,避免了頻繁調試。總之,我同意ARC作者的看法:應該選擇更具表現力、含義更爲豐富的詞語。

  當然,特定不等於標新立異或者聳人聽聞。友人goldlion曾經在學習NDK開發時被Android的詩意文檔所苦。當時我看到“punch a hole”這個表述(參見這裏,類的概覽部分,第二段首句),就笑得三分鐘沒停下來,是有點可愛。文檔可愛一點還好,如果具體的函數就麻煩了,比如ARC作者所提到的PHP的explode()函數。初看莫名其妙,定神想了想才明白可能是用於打散字符串用的。如果溫柔一點兒,應該叫做split或者delimit。而且更有趣的則是新支持的第三個參數。

  1. array explode ( string $delimiter , string $string [, int $limit ] )

  這個參數如果取負值,則最後的-limit組小字符串會被丟棄,例如

  1. explode('|''one|two|three|four', -1)  

  只會返回“one、two、three”三個子串所合成的數組。這種一魚兩吃的豪爽頗有古典程序員的遺風。不過我還是建議在工作代碼中將這種特定的處理命名爲splitButLast(char delimiter, String str, int thrownCount)更清爽,這樣一來寫的人和看的人都不累。

2.避免空泛的名稱

  tmp(temp)和retVal(returnValue、result)是十大空泛名稱排行榜上的前兩名(其餘請讀者補充)

  1. public double euclideanNorm(int values){ 
  2.   double result=0.0
  3.   for(int i=0,count<values.length;i<count;i++) 
  4.     result+=values[i]*values[i]; 
  5.   } 
  6.   return Math.sqrt(result); 

  這種命名不當我也常犯,第一句不假思索就用result了。上述代碼的result應該被squareSum代替,這樣一旦將for循環中的代碼誤寫爲squareSum+=values[i](忘記求平方了,直接加),立刻就能看出錯誤來。因爲sum前面的square已經明示了+=運算符後面必須是平方形式。

  temp這種名字也不是不能用。如果某個變量唯一存在目的就是交換數據的暫存空間,那麼也很貼切。

  1. if (right < left) { 
  2.   temp = right; 
  3.   right = left; 
  4.   left = temp; 

  反之如果是

  1. String temp = user.name(); 
  2. temp += " " + user.phoneNumber(); 
  3. temp += " " + user.email(); 
  4. ... 
  5. template.set("user_info", temp); 

  那麼以上代碼的temp就明顯是userInfo的偷懶寫法了,必須糾正。

  有時可以使用temp修飾另一箇中心詞,將此偏正短語作爲標識符,倒也恰當,比如:

  1. tempFile = namedTemporaryFile(); 
  2. ... 
  3. saveData(tempFile, ...); 

  temp修飾了File,如果僅用saveData(temp, ...),人們要去猜temp到底是臨時文件本身,還是臨時文件名,又或是被寫入的臨時數據?

  在循環語句所使用的迭代變量中,尤其要注意命名問題。空泛的i、j、k有時合適,有時則不行。尤其是會導致下標錯亂的情況下,更要注意循環變量的起名。例如:

  1. for (int i = 0; i < clubs.size(); i++) 
  2.   for (int j = 0; j < clubs[i].members.size(); j++) 
  3.     for (int k = 0; k < users.size(); k++) 
  4.       if (clubs[i].members[k] == users[j]) 
  5.         System.out.println("user[" + j + "] is in club[" + i + "]");

  很難注意到其中的bug,如果寫成

  1. if (clubs[ci].members[ui] == users[mi])  

  一下子就看到問題所在了。members數組的下標居然是ui(user index),users的下標居然是mi(member index),很明顯,這兩個寫反了。

3.名稱對內容的描述要具體而準確

  比如經常會定義如下的宏來防止生成默認的拷貝構造器與複製操作符。

  1. #define DISALLOW_EVIL_CONSTRUCTORS(ClassName) \ 
  2. ClassName(const ClassName&); \ 
  3. void operator=(const ClassName&); 

  這個evil constructors就太過感情化,不具體(怎麼evil了?),而且不甚準確(operator=並不是一個構建子)。所以莫如更爲精確的好:

  1. #define DISALLOW_COPY_AND_ASSIGN(ClassName) ...  

  上文一望即知:禁止提供拷貝構造器和賦值操作符。

  正交性也是考量準確度的一個標準。比如在設計參數選項時,經常會犯這樣的錯誤:有時候我們開發的某個手機程序需要打印調試信息到手機屏幕,同時需要屏蔽內嵌的程序廣告,有些小朋友以爲,開發的時候總是用模擬器來運行程序,所以就把這兩個功能強行塞入一個對應的選項中,並命名爲on_emulator。這樣的話有時候需要在真機上運行程序,而且要看調試信息,那麼不得不把on_emulator選項設定爲true。這看起來很容易造成誤解,而且一旦這樣設計,如果在真機上即要打印調試信息,同時還要顯示內嵌廣告,那麼on_emulator便怎麼設置都不對了。所以常犯的錯誤就是:根據表面現象,將兩個毫不相關或可以各自獨立存在的功能強行塞入一個選項中,既造成了誤解,又喪失了使用的靈活度。上述這種情況莫如分別設計成print_debug_on_screen和show_ads比較好。

4.將重要信息納入名稱中

  如果某個附加信息,代碼使用者非得知道它,才能正確地使用代碼的話,那它就得被納入標識符的名稱當中了。比如:

  1. String id; // 使用範例: "af84ef845cd8"  

  如果id一定要用十六進制字符串,否則後續程序無法正常執行的話,那麼這個信息必須讓大家知道。所以最好將代碼改成:

  1. String hexID;  

  這樣的話,大家看到了hex前綴,都會明白代碼作者的本意:非使用十六進制字符串不可。

  除了進制信息,計量的單位也應該被納入命名之中。 例如:

  1. long start = (new Date()).getTime();  
  2. ... 
  3. long elapsed = (new Date()).getTime() - start; 
  4. System.out.println("Load time was: " + elapsed + " seconds"); 

  上面這段代碼很容易出錯,因爲elapsed並沒有指明計時單位,是微秒?毫秒?秒?還是分鐘?小時?如果加上了計量單位:

  1. long startMs = (new Date()).getTime();  
  2. ... 
  3. long elapsedMs = (new Date()).getTime() - start; 
  4. System.out.println("Load time was: " + elapsedMs/1000 + " seconds"); 

  這樣的代碼一目瞭然。而且有了錯誤也非常好查找。萬一把“elapsedMs/1000”錯寫成“elapsedMs”,那麼一眼就能看到:明明後面是“seconds”,前面卻是“Ms”,單位明顯不統一,當即知道漏掉了“/1000”。

  根據以上這個例子,我們建議將左邊的參數改爲右邊的式樣:

  1. public void start(int delay ){...}; //delay改爲delaySecs 
  2. public void createCache(int size){...}; //size改爲sizeMB 
  3. public void throttleDownload(float limit){...}; //limit 改爲maxKBPS 
  4. public void rotate(float angle){...}; //angle改爲degreesClockwise 

  上面之中的第4條最爲嚴重。angle既沒說是角度還是弧度,又沒說是順時針還是逆時針,如果不配合詳細的Javadoc說明文檔,很難一眼讀透該方法所要表達的意思。

  除了計量單位之外,其餘代碼讀者或代碼使用者必須注意的信息也要納入命名之中。這樣以後該部分若有變動,可以在重構時及時更動變量名及使用它的其他語句,以維護代碼語義的一致性。例如:明文密碼應該叫plaintextPassword,以提醒使用者加密後方可使用,不宜直接叫做password。以後如果決定將初始的代碼由明文變爲已經加密好的,那麼只需要使用開發環境的重構功能將plaintextPassword變爲encryptedPassword即可,然後藉助開發工具找出所有使用encryptedPassword的地方,一一對照,如有邏輯不符,即行修改——這樣就維護了代碼邏輯的一致性,不會因爲是否加密而導致bug或程序行爲改變。同理,用戶提供的註釋裏面可能包含需要進行轉義處理的字符,此時應叫unescapedComment而非comment;已經轉換爲UTF-8格式的html字節序應叫htmlUTF8而非html;經由URL編碼形式傳入的數據應叫dataURLEnc而非data。

  很久以前,我也是一名Win32的API研究愛好者,當然忘不了匈牙利命名法了,那麼“將重要信息納入名稱中“與”匈牙利命名法“有何區別呢?(這裏主要講的是系統匈牙利命名法,另外一種叫匈牙利應用命名法——感謝網友李先生在原文樓下評論中指出此問題)它們的區別是,後者是一套正規的強制規範,納入名稱中的一般是指針(p)、映射表(m)、零終結字符串(sz)、計數(c)等特定屬性,而前者則無此強制屬性規定,凡對用戶重要的屬性均可納入。可以仿稱其爲“要素命名法”("Essential Factor Notation")。(ARC的作者用“English Notation”來命名它,小翔覺之不確)

5.標識符的長短應符合其作用域的大小。

  1. if (debug) { 
  2.   Map m=...; 
  3.   ... 
  4.   print(m); 

  變量m的作用域很小,所以短命稱不會帶來問題。但是如果是在一個很大的作用域中,比如有上千行代碼的類中:

  1. public class PhoneBook{ 
  2.   private Map m=...; 
  3.   ... //幾千行代碼之後 
  4.   public void someFun(){ 
  5.     ... 
  6.     print(m); // m是啥咪東東呀? 
  7.     ... 
  8.   } 
  9.   ... //還有數千行代碼 

  那麼m這樣的短名顯然不太合適。現在的編輯環境一般都有自動補完功能,按下某個組合鍵就好了,比如常見的幾種編輯器:

編輯器/開發環境 自動補完快捷鍵
Vi Ctrl-p
Emacs Meta-/
Eclipse Alt-/
IntelliJ IDEA Alt-/
TextMate ESC

  我常用的是eclipse,其餘的歡迎大家補充。

  當然啦,將不必要的詞彙省略是好的。例如convertToString()簡稱toString(),doServerLoop()簡稱serverLoop()。翔以爲主要是將不言自明的動詞(比如convert,do等)省去。

6.使用格式來傳達信息

  使用特殊的符號來表示特殊的對象,同其他普通對象區隔開來。例如在JavaScript中,用$爲前綴來表示經由jQuery的$("...")選擇子而選中的一系列具有某名稱的DOM節點。(小翔對JS不是很熟悉,因爲日常工作是單機的手機應用/遊戲開發。目前正在學習中,這部分代碼有錯誤還望朋友們賜教)

  1. var $all_images = $("img"); // $all_images是jQuery對象 
  2. var height = 250;//而height則是普通變量 

  每種特殊標識符都用一套特殊命名法來區隔。例如HTML/CSS中,id與class都是特殊屬性,所以分別採用下劃線與連字符來命名這兩種標識符。(再次捂臉:HTML/CSS苦手飄過,仍然是在努力學習這項技術之中)例如:

  1. <div id="middle_column" class="main-content"> ...

  嗯,寫了這麼多,休息一下吧。輕鬆地總結一下啦:

  ”以語句行爲單位的微觀代碼管控如何入手呢?”“必也正名乎!”——將信息納入名稱,使讀者通過名字就能領會到其中的含義。

  特定技巧:

  1. 使用更具表達力詞語:例如以在BinaryTree類的設計中以height或nodeCount代替size。
  2. 避免空泛名稱:tmp、retval、i、j、k等,除非確有必要,否則不用。
  3. 使用具體而準確的名稱:描述更多細節的CanListenPort()優於ServerCanStart()。
  4. 附加重要屬性:將Ms綴於以毫秒計時的值名稱之後,將Raw綴於未經處理的數據名稱之前。
  5. 大作用域用長名:不要把一兩個字符的名稱用在一大段代碼中,短的代碼可以有短名。
  6. 特殊名稱用特殊格式:類成員可以_結尾,以與局部變量相區隔。$符號、大寫或下劃線等特殊格式可以區隔特殊的名稱。

  嗯,這篇文章寫了好幾個小時,休息一下。正名大業分爲上下兩部分,這一篇主要是從正面給大家總結一些標識符命名的建議,下一篇則將從反面講解何種名稱會給人帶來誤解。

愛飛翔 2012年6月1日-2日

本文使用Creative Commons BY-NC-ND 3.0協議(創作共用 自由轉載-保持署名-非商業使用-禁止衍生)發佈。

本文網址:http://agilemobidev.net/eastarlee/code-quality/think_in_code_quality_2_name_zh_cn/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章