不必被我的標題嚇到哈,,孔老夫子時代沒有電腦,。如果有,估計諸子百家們還得針對軟件工程抒發(fā)一系列代碼質(zhì)量倫理學的教條,。 上回文章說到,,代碼品質(zhì)改進應該在三個層面上展開,其中最微觀的就是代碼段的質(zhì)量考究了,。很多時候我在針對一些項目做工程分析和大規(guī)模重構(gòu)之 前,,首先希望對大概的工作原理有些了解,這個時候就要深入核心模塊的文件之中,挑選代碼來閱讀,,以求理順思路了,。根據(jù)個人的經(jīng)驗來說,微觀的改進往往能夠 激發(fā)大規(guī)模的結(jié)構(gòu)重組,。所以一連幾篇文章,分別會談到“好名稱”,、“好格式”,、“好注釋”三個微觀的表層質(zhì)量改進問題。 深入到函數(shù)或方法內(nèi)部的代碼之后,,就要面對一行行具體的代碼了,。此時最應該關(guān)注的首先就是標識符的命名問題。這個問題基本上是講重構(gòu)或代碼質(zhì)量 的書所必談的話題之一,。記得馬叔叔曾經(jīng)在《Clean Code》中說,,給標識符起名時,應該像給你們家小朋友起名字一樣認真(大意,,并非原文),。當時我看到此話不禁微笑了一下。是哇,,很多時候我在代碼評審中 遇到的思維不順都是源于名字問題,。 一直以來,朋友和同事都偶爾會拿整個項目或是代碼片段來和我討論,,對于企業(yè)級開發(fā)領(lǐng)域,,我看的代碼不多,對代碼質(zhì)量不便妄言,,不過具體到和我關(guān) 系比較密切的移動開發(fā)領(lǐng)域,,可就真的是令我非常頭疼了。由于移動軟件或游戲的開發(fā)經(jīng)常周期很短,,而且重結(jié)果,,輕過程,更不講求后續(xù)的版本更新,、維護與復 用,。所以經(jīng)常在開發(fā)過程中程序員容易在工期的壓力下過于隨心所欲,導致項目的代碼理解起來大費周折,。有時候我越是急于理解,,就越是摸不著頭緒。后來想想,, 很多困難都源于具體的標識符名稱,。必須理解了它們,才有可能理解更高層級的內(nèi)容,。 通過閱讀《The Art of Readable Code》以及其他相關(guān)的書,,我漸漸把原來學到的一些代碼質(zhì)量知識總結(jié)起來了,。ARC這本書的好處之一就是,它講的東西不見得多新,,很多都是Clean Code或者類似的書中講了又講的話題,,不過,它善于把這些零散的知識點按照一定的框架整合起來,,讓我能夠更系統(tǒng)地歸納并鞏固這些知識,。 簡單的說,好的標識符名稱,,必須封裝恰當?shù)男畔?,同時不致誤解。 至于如何封裝恰當?shù)男畔?,這個問題要看個人的把握,,有幾條能夠作為指導的建議,不妨梳理給大家來看,。 1. 選擇更具表達力詞語 我自己在代碼中就經(jīng)常忽視這一點,,用慣了get和size之后,遇到什么情況,,不管具體細節(jié),,一律使用getXXX或size作為方法名稱。今天就看到了幾個反例,。例如 class BinaryTree{ public int size(){...}} 這個size到底獲取的是高度,,節(jié)點數(shù)還是占據(jù)的內(nèi)存字節(jié)數(shù)?這三種情況應該分別用更為特定的height,、nodeCount或occupiedMemoryBytes來表示,,而不是空泛的size。 說到這個問題,,我覺得增加個人的詞匯量是非常有好處的,。可以經(jīng)常翻看英英詞典來了解各個詞語之間的細微差別,。例如用“deliver, dispatch, announce, distribute, route”(投遞,、派發(fā)、播報,、分配,、按指定線路發(fā)送,就是路由)之中的某個詞代替send(送),,用“search, extract, locate, recover”(搜索,、提取、定位、重新找回)代替find(找)等等,。 有一個問題,,就是命名含義豐富了會不會影響以后的修改。有同學可能會說,,我故意放一個朦朧且曖昧的size來代替height,、 nodeCount或occupiedMemoryBytes,這樣將來萬一內(nèi)部的邏輯有變化,,我直接修改具體代碼就行了,,連size這個方法名都不用修 改,豈不是更符合“針對接口而非實現(xiàn)來編程”的面向?qū)ο笤O計理論么,?一開始我也有這個想法,后來想想后果十分可怕,,這樣做根本就沒有明確表述出該接口的具 體意圖:一旦將表示height的size方法之中的算法改為返回nodeCount,,而保留size方法名不做修改,那么這會害苦了該API的客戶代碼 編寫者們,。 你的同事仍然以為size返回的是二叉樹的高度,,殊不知現(xiàn)在它返回的是節(jié)點數(shù)目了。一旦出現(xiàn)這樣的bug,,除非兩人緊密配合,,否則調(diào)試很費時, 而且隨著時間的推移更為難辦,。反之如果方法名從height改為nodeCount,,那么下游開發(fā)者在源碼管理系統(tǒng)中更新代碼時立刻就看出其中的差別,從 而能夠很從容地修改已有的邏輯,,避免了頻繁調(diào)試,。總之,,我同意ARC作者的看法:應該選擇更具表現(xiàn)力,、含義更為豐富的詞語。 當然,,特定不等于標新立異或者聳人聽聞,。友人goldlion曾經(jīng)在學習NDK開發(fā)時被Android的詩意文檔所苦。當時我看到“punch a hole”這個表述(參見這里,, 類的概覽部分,,第二段首句),就笑得三分鐘沒停下來,,是有點可愛,。文檔可愛一點還好,如果具體的函數(shù)就麻煩了,比如ARC作者所提到的PHP的 explode()函數(shù),。初看莫名其妙,,定神想了想才明白可能是用于打散字符串用的。如果溫柔一點兒,,應該叫做split或者delimit,。而且更有趣 的則是新支持的第三個參數(shù)。 array explode ( string $delimiter , string $string [, int $limit ] ) 這個參數(shù)如果取負值,,則最后的-limit組小字符串會被丟棄,,例如 explode('|', 'one|two|three|four', -1) 只會返回“one、two,、three”三個子串所合成的數(shù)組,。這種一魚兩吃的豪爽頗有古典程序員的遺風。不過我還是建議在工作代碼中將這種特 定的處理命名為splitButLast(char delimiter, String str, int thrownCount)更清爽,,這樣一來寫的人和看的人都不累,。 2. 避免空泛的名稱 tmp(temp)和retVal(returnValue、result)是十大空泛名稱排行榜上的前兩名(其余請讀者補充) public double euclideanNorm(int values){ double result = 0.0; for(int i = 0, count < values.length; i < count;i++) result += values[i]*values[i]; } return Math.sqrt(result); } 這種命名不當我也常犯,,第一句不假思索就用result了,。上述代碼的result應該被squareSum代替,這樣一旦將for循環(huán)中的代 碼誤寫為squareSum+=values[i](忘記求平方了,,直接加),,立刻就能看出錯誤來。因為sum前面的square已經(jīng)明示了+=運算符后 面必須是平方形式,。 temp這種名字也不是不能用,。如果某個變量唯一存在目的就是交換數(shù)據(jù)的暫存空間,那么也很貼切,。 if (right < left) { temp = right; right = left; left = temp; } 反之如果是 String temp = user.name(); temp += " " + user.phoneNumber(); temp += " " + user.email(); ... template.set("user_info", temp); 那么以上代碼的temp就明顯是userInfo的偷懶寫法了,,必須糾正。 有時可以使用temp修飾另一個中心詞,,將此偏正短語作為標識符,,倒也恰當,比如: tempFile = namedTemporaryFile();
...
saveData(tempFile, ...);
temp修飾了File,,如果僅用saveData(temp, …),,人們要去猜temp到底是臨時文件本身,還是臨時文件名,,又或是被寫入的臨時數(shù)據(jù),? 在循環(huán)語句所使用的迭代變量中,尤其要注意命名問題,??辗旱膇,、j、k有時合適,,有時則不行,。尤其是會導致下標錯亂的情況下,更要注意循環(huán)變量的起名,。例如: for (int i = 0; i < clubs.size(); i++) for (int j = 0; j < clubs[i].members.size(); j++) for (int k = 0; k < users.size(); k++) if (clubs[i].members[k] == users[j]) System.out.println("user[" + j + "] is in club[" + i + "]"); 很難注意到其中的bug,,如果寫成 if (clubs[ci].members[ui] == users[mi])
一下子就看到問題所在了。members數(shù)組的下標居然是ui(user index),,users的下標居然是mi(member index),,很明顯,這兩個寫反了,。 3. 名稱對內(nèi)容的描述要具體而準確 比如經(jīng)常會定義如下的宏來防止生成默認的拷貝構(gòu)造器與復制操作符,。 #define DISALLOW_EVIL_CONSTRUCTORS(ClassName) \ ClassName(const ClassName&); void operator=(const ClassName&); 這個evil constructors就太過感情化,不具體(怎么evil了,?),,而且不甚準確(operator=并不是一個構(gòu)建子)。所以莫如更為精確的好: #define DISALLOW_COPY_AND_ASSIGN(ClassName) ...
上文一望即知:禁止提供拷貝構(gòu)造器和賦值操作符,。 正交性也是考量準確度的一個標準。比如在設計參數(shù)選項時,,經(jīng)常會犯這樣的錯誤:有時候我們開發(fā)的某個手機程序需要打印調(diào)試信息到手機屏幕,,同時 需要屏蔽內(nèi)嵌的程序廣告,有些小朋友以為,,開發(fā)的時候總是用模擬器來運行程序,,所以就把這兩個功能強行塞入一個對應的選項中,并命名為 on_emulator,。這樣的話有時候需要在真機上運行程序,,而且要看調(diào)試信息,那么不得不把on_emulator選項設定為true,。這看起來很容 易造成誤解,,而且一旦這樣設計,如果在真機上即要打印調(diào)試信息,,同時還要顯示內(nèi)嵌廣告,,那么on_emulator便怎么設置都不對了。所以常犯的錯誤就 是:根據(jù)表面現(xiàn)象,,將兩個毫不相關(guān)或可以各自獨立存在的功能強行塞入一個選項中,,既造成了誤解,又喪失了使用的靈活度,。上述這種情況莫如分別設計成 print_debug_on_screen和show_ads比較好,。 4. 將重要信息納入名稱中 如果某個附加信息,,代碼使用者非得知道它,才能正確地使用代碼的話,,那它就得被納入標識符的名稱當中了,。比如: String id; // 使用范例: "af84ef845cd8" 如果id一定要用十六進制字符串,否則后續(xù)程序無法正常執(zhí)行的話,,那么這個信息必須讓大家知道,。所以最好將代碼改成: String hexID; 這樣的話,大家看到了hex前綴,,都會明白代碼作者的本意:非使用十六進制字符串不可,。 除了進制信息,計量的單位也應該被納入命名之中,。 long start = (new Date()).getTime(); ... long elapsed = (new Date()).getTime() - start; System.out.println("Load time was: " + elapsed + " seconds"); 上面這段代碼很容易出錯,,因為elapsed并沒有指明計時單位,是微秒,?毫秒,?秒?還是分鐘,?小時,?如果加上了計量單位: long startMs = (new Date()).getTime(); ... long elapsedMs = (new Date()).getTime() - start; System.out.println("Load time was: " + elapsedMs/1000 + " seconds"); 這樣的代碼一目了然。而且有了錯誤也非常好查找,。萬一把“elapsedMs/1000”錯寫成“elapsedMs”,,那么一眼就能看到:明明后面是“seconds”,前面卻是“Ms”,,單位明顯不統(tǒng)一,,當即知道漏掉了“/1000”。 根據(jù)以上這個例子,,我們建議將左邊的參數(shù)改為右邊的式樣: public void start(int delay ){...}; //delay改為delaySecs public void createCache(int size){...}; //size改為sizeMB public void throttleDownload(float limit){...}; //limit 改為maxKBPS public void rotate(float angle){...}; //angle改為degreesClockwise 上面之中的第4條最為嚴重,。angle既沒說是角度還是弧度,又沒說是順時針還是逆時針,,如果不配合詳細的Javadoc說明文檔,,很難一眼讀透該方法所要表達的意思。 除了計量單位之外,,其余代碼讀者或代碼使用者必須注意的信息也要納入命名之中,。這樣以后該部分若有變動,可以在重構(gòu)時及時更動變量名及使用它的 其他語句,,以維護代碼語義的一致性,。例如:明文密碼應該叫plaintextPassword,以提醒使用者加密后方可使用,,不宜直接叫做 password,。 以后如果決定將初始的代碼由明文變?yōu)橐呀?jīng)加密好的,,那么只需要使用開發(fā)環(huán)境的重構(gòu)功能將plaintextPassword變?yōu)? encryptedPassword即可,然后藉助開發(fā)工具找出所有使用encryptedPassword的地方,,一一對照,,如有邏輯不符,即行修改 ——這樣就維護了代碼邏輯的一致性,,不會因為是否加密而導致bug或程序行為改變,。同理,用戶提供的注釋里面可能包含需要進行轉(zhuǎn)義處理的字符,,此時應叫 unescapedComment而非comment,;已經(jīng)轉(zhuǎn)換為UTF-8格式的html字節(jié)序應叫htmlUTF8而非html;經(jīng)由URL編碼形式 傳入的數(shù)據(jù)應叫dataURLEnc而非data,。 很久以前,,我也是一名Win32的API研究愛好者,當然忘不了匈牙利命名法了,,那么“將重要信息納入名稱中“與”匈牙利命名法“有何區(qū)別呢,?它們的區(qū)別是,后者是一套正規(guī)的強制規(guī)范,,納入名稱中的一般是指針(p),、映射表(m)、零終結(jié)字符串(sz),、計數(shù)(c)等特定屬性,,而前者則無此強制屬性規(guī)定,凡對用戶重要的屬性均可納入,。可以仿稱其為“要素命名法”(”Essential Factor Notation”),。(ARC的作者用“English Notation”來命名它,,小翔覺之不確) 5. 標識符的長短應符合其作用域的大小 if (debug) { Map<String, int> m=...; ... print(m); } 變量m的作用域很小,所以短命稱不會帶來問題,。但是如果是在一個很大的作用域中,,比如有上千行代碼的類中: public class PhoneBook{ private Map<String, int> m=...; ... //幾千行代碼之后 public void someFun(){ ... print(m); // m是啥咪東東呀? ... } ... //還有數(shù)千行代碼 } 那么m這樣的短名顯然不太合適?,F(xiàn)在的編輯環(huán)境一般都有自動補完功能,,按下某個組合鍵就好了,比如常見的幾種編輯器:
我常用的是eclipse,,其余的歡迎大家補充,。 當然啦,將不必要的詞匯省略是好的,。例如convertToString()簡稱toString(),,doServerLoop()簡稱serverLoop(),。翔以為主要是將不言自明的動詞(比如convert,do等)省去,。 6. 使用格式來傳達信息 使用特殊的符號來表示特殊的對象,,同其他普通對象區(qū)隔開來。例如在JavaScript中,,用$為前綴來表示經(jīng)由jQuery的$(“…”)選 擇子而選中的一系列具有某名稱的DOM節(jié)點,。(小翔對JS不是很熟悉,因為日常工作是單機的手機應用/游戲開發(fā),。目前正在學習中,,這部分代碼有錯誤還望朋 友們賜教) var $all_images = $("img"); // $all_images是jQuery對象 var height = 250; //而height則是普通變量 每種特殊標識符都用一套特殊命名法來區(qū)隔。例如HTML/CSS中,,id與class都是特殊屬性,,所以分別采用下劃線與連字符來命名這兩種標識符。(再次捂臉:HTML/CSS苦手飄過,,仍然是在努力學習這項技術(shù)之中)例如: <div id="middle_column" class="main-content"> ...
嗯,,寫了這么多,休息一下吧,。輕松地總結(jié)一下啦: ”以語句行為單位的微觀代碼管控如何入手呢,?”“必也正名乎!”——將信息納入名稱,,使讀者通過名字就能領(lǐng)會到其中的含義,。 特定技巧:
嗯,,這篇文章寫了好幾個小時,,休息一下。正名大業(yè)分為上下兩部分,,這一篇主要是從正面給大家總結(jié)一些標識符命名的建議,,下一篇則將從反面講解何種名稱會給人帶來誤解。 |
|
來自: rookie > 《技術(shù)帖》