四個(gè)有害的Java編碼習(xí)慣
程序中的編碼風(fēng)格讓我們的編程工作變得輕松,特別是程序維護(hù)員,他們要經(jīng)常閱讀其他人編寫的程序編碼,這一點(diǎn)尤其突出。編碼規(guī)范從根本上解決了程序維護(hù)員的難題;規(guī)范的編碼閱讀和理解起來(lái)更容易,也可以快速的不費(fèi)力氣的借鑒別人的編碼。對(duì)將來(lái)維護(hù)你編碼的人來(lái)說(shuō),你的編碼越優(yōu)化,他們就越喜歡你的編碼,理解起來(lái)也就越快。
同樣,高水平的編碼風(fēng)格(例如固定的封閉結(jié)構(gòu))目的在于改善設(shè)計(jì)和使編碼更易于理解。事實(shí)上,最后有些人會(huì)認(rèn)為改善設(shè)計(jì)和提高編碼的易讀性是一回事。
本文中你會(huì)看到一些流行的編碼風(fēng)格被面向讀者的更易于接受的風(fēng)格所替代。有人爭(zhēng)論說(shuō)這些風(fēng)格都已經(jīng)被大家廣泛使用,不應(yīng)該簡(jiǎn)單的為了達(dá)到讀者的期望而拋棄。然而,讀者的期待只是其中一方面的原因,不可能凌駕于所有因素之上。列出四種常見的問(wèn)題:
1.對(duì)局域變量(local variables)、參數(shù)(method arguments)、字段(fields)這三種變量的命名沒有區(qū)分:
對(duì)看編碼的人來(lái)說(shuō),首先要弄清這些數(shù)據(jù)如何定義的?看一個(gè)類時(shí),得弄清楚每個(gè)條目是局域變量?字段?還是參數(shù)?有必要使用一個(gè)簡(jiǎn)單的命名約定來(lái)定義這些變量,增加易讀性。
很多權(quán)威機(jī)構(gòu)規(guī)范過(guò)字段變量用以區(qū)分它與其它的變量,但這遠(yuǎn)遠(yuǎn)不夠。可以把對(duì)字段的合理的命名約定邏輯也應(yīng)用在參數(shù)上面。先看示例1:沒有進(jìn)行區(qū)分這三種變量的類定義,如下所示:
示例1:
public boolean equals (Object arg) { if (! (arg instanceof Range)) return false; Range other = (Range) arg; return start.equals(other.start) && end.equals(other.end);} |
在這個(gè)方法中,arg直接用argument的縮寫,雖然大家一看就知道這是參數(shù)了,但這種命名方式卻丟失了參數(shù)代表的對(duì)象本身 的含義。大家知道這 是參數(shù),卻不知道這是什么參數(shù)。如果方法的參數(shù)多一點(diǎn),都按照arg1,arg2這樣的方式命名,閱讀代碼 的時(shí)候很頭疼。另外兩個(gè)字段變量,start和 end,突然憑空而出,想一下才知道這應(yīng)該是字段。當(dāng)然,這個(gè)方法很短,造成的困難還不大,如果這個(gè)方法比較長(zhǎng)的話,突然看到start和end兩個(gè)變 量,一般會(huì)先在前面找一下是不是局部變量,然后才能確定是類的字段變量。
這個(gè)問(wèn)題貌似微不足道,但為什么要讓代碼閱讀者花費(fèi)額外時(shí)間在這些瑣碎的問(wèn)題上呢?如果有個(gè)方案能讓代碼閱讀者一目了然的明白變量是那種變量,為什 么不采用呢?就如同Steve McConnell在 《代碼大全》中說(shuō)的:"讓人費(fèi)神去琢磨神秘殺人兇手這沒有問(wèn)題,但你不需要琢磨程序代碼,代碼是用來(lái)閱讀的。
接下來(lái)看示例2,使用命名約定后對(duì)示例1重寫以后的代碼,用到的命名約定有:
◆參數(shù)定義時(shí)名字加前綴a
◆字段定義時(shí)名字加前綴f
◆局域變量定義時(shí)不加任何前綴
示例2:對(duì)變量類型進(jìn)行區(qū)分
public boolean equals (Object aOther) { if (! (aOther instanceof Range)) return false; Range other = (Range) aOther; return fStart.equals(other.fStart) && fEnd.equals(other.fEnd);} |
你可能反對(duì)示例2中的風(fēng)格,反對(duì)過(guò)時(shí)了的匈牙利符號(hào),但是我認(rèn)為反對(duì)是錯(cuò)誤的,因?yàn)樾傺览?hào)能詳細(xì)說(shuō)明信息的類型。上面的命名約定區(qū)分了類型。而且這樣做分清了字段、變量和局域變量,這是兩種完全不同的概念。這種命名約定的方式并不像看起來(lái)那么微不足道:當(dāng)這些約定用在程序編碼中時(shí),會(huì)大大降低理解的難度,因?yàn)槟憧梢圆恍枰确直孢@些變量,省去不少時(shí)間。
2.按層次劃分包,而不是根據(jù)特征或功能劃分最常見的劃分應(yīng)用序就是按層次命名包:
com.blah.action 、com.blah.dao 、com.blah.model、com.blah.util
也就是說(shuō),把具有同樣特征或者功能的類劃分到了不同的包里。因?yàn)槌蓡T的屬性對(duì)其他成員應(yīng)該是可見的,這就意味著幾乎應(yīng)用程序中所有的類都是公共的。實(shí)際上,這種按層次劃分包的方法完全扔掉了Java的包內(nèi)私有。包內(nèi)私有應(yīng)該徹底不使用?,F(xiàn)在,包內(nèi)私有是Java程序語(yǔ)言中設(shè)計(jì)者的默認(rèn)作用域。這種包的劃分習(xí)慣也違反了面向?qū)ο缶幊痰暮诵脑瓌t之--盡量保持私有以減少影響,因?yàn)檫@種習(xí)慣強(qiáng)迫你必須擴(kuò)大類的作用域。由于一些奇怪的原因,一些Java組織不贊成這種命名,似乎不公正的。
另一種風(fēng)格是按特征劃分命名:
com.blah.painting 、com.blah.buyer 、com.blah.seller 、com.blah.auction 、com.blah.webmaster 、com.blah.useraccess 、com.blah.util
這里,成員不按行為劃分,而是按照不同特征的類劃分,每個(gè)成員都關(guān)聯(lián)不同的特征。這種方法下包在最初使用是被定義。
例如:在Web應(yīng)用程序中,“com.blah.painting”包可能由下列成員組成:
◆Painting.java: 一個(gè)model對(duì)象
◆PaintingDAO.java: 一個(gè)數(shù)據(jù)存取對(duì)象Dao
◆PaintingAction.java: 一個(gè)控制或者行為對(duì)象
◆statements.sql: Dao對(duì)象使用的SQl文件
◆view.jsp: Jsp文件
需要特別說(shuō)是的是,這種劃分方法,每一個(gè)包都包含所有成員有關(guān)的特征文件,而不僅僅是Java源文件。這種按特征劃分包的方法,要求在做刪除操作時(shí)要注意,刪除一個(gè)特征時(shí)要?jiǎng)h掉它的整個(gè)目錄,不能保存在源碼中。
這種方法優(yōu)于按層次劃分包的方法,表現(xiàn)在以下幾點(diǎn):
◆包是高內(nèi)聚的,并且模塊化,包與包之間的耦合性被降到最低。
◆代碼的自描述性增強(qiáng). 讀者只需看包的名字就對(duì)程序有些什么功能或特征有了大概的印象。在《代碼大全》中, Steve McConnell 將自描述性的代碼比作 "易讀的圣杯",來(lái)表達(dá)它的易讀性。
◆把類按照每個(gè)特征和功能區(qū)分開可以很容易實(shí)現(xiàn)分層設(shè)計(jì)。
◆相關(guān)的成員在同一個(gè)位置。不需要為了編輯一個(gè)相關(guān)的成員而去瀏覽整個(gè)源碼樹。
◆成員的作用域默認(rèn)是包內(nèi)私有。只有當(dāng)另外的包需要訪問(wèn)某個(gè)成員的時(shí)候,才把它修改為public. (需要注意的是修改一個(gè)類為public,并不意味著它的所有類成員都應(yīng)該改為public。public成員和包內(nèi)私有(package- private)成員是可以在同一個(gè)類里共存的。)
◆刪除一個(gè)功能或特征只需要簡(jiǎn)單的刪除一個(gè)文件夾。
◆每個(gè)包內(nèi)一般只有很少的成員,這樣包可以很自然的按照進(jìn)化式發(fā)展。如果包慢慢變的太大,就可以再進(jìn)行細(xì)分,把它重構(gòu)為兩個(gè)或者更多新的包,類似于物種進(jìn)化。而按照層次劃分的方式,就沒辦法進(jìn)化式發(fā)展,重構(gòu)也不容易。
一些框架推薦使用層層定義包的傳統(tǒng)的方式做為包的命名方法:由于使用傳統(tǒng)的包命名,開發(fā)者總能知道在哪個(gè)位置可以找到這些項(xiàng)目,但是為什么避免人們這樣做呢?使用另一種按特征定義包的風(fēng)格,就不需要這種單調(diào)的操縱,因此,按特征定義完全超越了任何其它命名約定。約書亞布洛赫在《高效的Java》一書中說(shuō)到:區(qū)分一個(gè)設(shè)計(jì)好壞的唯一重要因素是模塊內(nèi)部隱藏的數(shù)據(jù)和其它模塊中涉及的實(shí)現(xiàn)過(guò)程的程度。#p#
3.習(xí)慣用JavaBeans而不是不可變對(duì)象
不可變對(duì)象是構(gòu)造后狀態(tài)不改變。Scala的主要?jiǎng)?chuàng)造者M(jìn)artin Odersky最近還稱贊過(guò)這種不可變對(duì)象。在《高效的Java》一書中,Joshua Bloch列舉了大量實(shí)例支持使用不可變對(duì)象,并總結(jié)了很多優(yōu)點(diǎn)。但他的意見,似乎很大程度上被忽略。大多數(shù)程序使用JavaBeans來(lái)替代不可變對(duì)象。JavaBean明顯要比不可變對(duì)象復(fù)雜的多,因?yàn)樗木薮蟮穆暶骺臻g。粗略的講,你可以把JavaBean看作是與不可變對(duì)象完全相反的對(duì)象:它允許最大的可變性。JavaBean常被用來(lái)做數(shù)據(jù)庫(kù)記錄的映射。假如你要從數(shù)據(jù)庫(kù)記錄集映射一行為對(duì)象,不考慮現(xiàn)有的持久化方案和框架,你會(huì)將這個(gè)對(duì)象設(shè)計(jì)成什么樣子?跟javabean相似呢還是完全不一樣?我認(rèn)為會(huì)完全不一樣,說(shuō)明如下:
◆它不包含一個(gè)無(wú)參數(shù)構(gòu)造方法(這一特征是javabean必備的。)。作者認(rèn)為一個(gè)數(shù)據(jù)庫(kù)記錄的對(duì)象如果不包含任何數(shù)據(jù)是沒有意義的。一個(gè)數(shù)據(jù)庫(kù)表的所有字段都是可選的情況有多少?
◆It would likely not have anything to say about events and listeners.(不太明白作者的意思)
◆它不強(qiáng)迫你用可變的對(duì)象。
◆它內(nèi)部有一個(gè)數(shù)據(jù)驗(yàn)證機(jī)制。這樣一個(gè)驗(yàn)證機(jī)制對(duì)大多數(shù)數(shù)據(jù)庫(kù)應(yīng)用非常重要。(記住對(duì)象的第一原則:一個(gè)對(duì)象應(yīng)該同時(shí)封裝數(shù)據(jù)和對(duì)數(shù)據(jù)的操作。在這種情況下,操作就是驗(yàn)證數(shù)據(jù)。)
◆數(shù)據(jù)驗(yàn)證機(jī)制可以給最終用戶(end user)報(bào)錯(cuò)。
按照javabeans的說(shuō)明,javabeans是用來(lái)解決特殊領(lǐng)域的問(wèn)題:在圖形界面程序的設(shè)計(jì)中充當(dāng)小部件。說(shuō)明中絕對(duì)沒有提到數(shù)據(jù)庫(kù)。但現(xiàn)在通常用javabean來(lái)做數(shù)據(jù)庫(kù)記錄的映射。從實(shí)際角度來(lái)講,許多被廣泛使用的框架要求應(yīng)用程序使用JavaBeans(或者其它類似的規(guī)范)來(lái)映射數(shù)據(jù)庫(kù)記錄。這種濫用不利于編程者了解和使用不可變對(duì)象。
4.私有成員排在其它成員的前面類成員的排序沒有按照成員的作用域的大小排列,而是把private放在前面。
以前的好萊塢影片開頭總是長(zhǎng)篇的榮譽(yù)。同樣地,大多數(shù)Java類把私有成員放在最前面。示例3給出這種風(fēng)格的典型例子:
public class OilWell implements EnergySource { private Long id; private String name; private String location; private Date discoveryDate; private Long totalReserves; private Long productionToDate; public Long getId() { return id; } public void setId(Long id) { this.id = id; } //..elided} |
然而,如果把私有成員定義放在后面,讀者閱讀會(huì)更容易。因?yàn)槿藗冋J(rèn)識(shí)一個(gè)事物的通常過(guò)程都是從一般到特殊,從抽象層次來(lái)說(shuō),是從高層次到低層次的認(rèn)識(shí)過(guò)程。如果你倒過(guò)來(lái)的話,讀者就不能從整體上把握事物,也不能抓住事物的本質(zhì),只能在一堆具體的片段中迷失。
整體的抽象讓你忽略了細(xì)節(jié)。抽象的層次越高,你可以忽略越多的細(xì)節(jié)。讀者閱讀一個(gè)類時(shí)可以忽略的細(xì)節(jié)越多他會(huì)越高興。腦袋里填充太多的細(xì)節(jié)是痛苦的,所以細(xì)節(jié)越少越好。因此,將私有成員放在最后會(huì)顯得更富有同情心,因?yàn)檫@樣阻止了不必要的細(xì)節(jié)顯露給讀者。
本來(lái)C++程序的習(xí)慣也是像Java一樣把private成員放在最開始。然而,C++社區(qū)迅速的認(rèn)識(shí)到這是一個(gè)有害的規(guī)范,這個(gè)規(guī)范現(xiàn)在已經(jīng)被修正。這里給出一個(gè)經(jīng)典的C++風(fēng)格指南里的注釋:
注意:public 接口應(yīng)該放在class的最開始,其次是protected成員,最后是private成員。原因是:
◆程序員應(yīng)該更關(guān)心接口而不是具體實(shí)現(xiàn)。
◆當(dāng)程序員需要用一個(gè)類的時(shí)候,他們需要的是接口而不是實(shí)現(xiàn)。
把接口放在開始是非常有意義的。把實(shí)現(xiàn)部分,私有部分,放在開始是一個(gè)歷史遺留問(wèn)題。最后還是要反復(fù)強(qiáng)調(diào)一下,一個(gè)類的接口的重要性超過(guò)實(shí)現(xiàn)細(xì)節(jié)。
同樣,倫敦大學(xué)帝國(guó)學(xué)院關(guān)于C++的指面中也說(shuō)到:把公有的部分放在前面,讀者會(huì)更感興趣閱讀,然后是保護(hù)的部分,最后是私有的部分。
有人會(huì)持反對(duì)意見,認(rèn)為讀者可以使用程序文檔來(lái)理解類,而不是直接看源代碼。這種理由似乎不成立,因?yàn)槌绦蛭臋n中沒有相關(guān)的實(shí)現(xiàn)細(xì)節(jié),這時(shí)看源代碼是很有必要的。
所有的技術(shù)文檔,通常都把難理解的信息放在開頭,比如抽象的學(xué)術(shù)論文。為什么Java不打破這種常規(guī)呢?把私有成員放在最開頭部分看起來(lái)是不是打破常規(guī)的好習(xí)慣。這種習(xí)慣似乎是sun早期的編碼規(guī)范造成的。
將代碼按照javadoc的順序編排是非常好的:首先是構(gòu)造方法,然后是非私有方法,最后是私有部分和方法。這樣讀者閱讀的時(shí)候很自然的從抽象層次的高向低運(yùn)動(dòng)。
本文所講的是一些Java的不好習(xí)慣和風(fēng)格需要改變。最終的目地是希望我們的代碼易讀性更強(qiáng),讓讀者更易于理解。
【編輯推薦】