四個有害的 Java 習慣用法

 四個有害的 Java 習慣用法和如何克服它們

原本地址:http://www.javaworld.com/javaworld/jw-07-2008/jw-07-harmful-idioms.html?page=1 

譯註:個人認爲文中觀點極好,特翻譯一下,與大家共享。對於文中錯誤之處、不通之處、不妥之處,請隨時提出意見或建議。由於白天極累,所以腦子有點打漿,容易出錯。儘管已經校對,但錯誤難免,請多包涵! 

任何語言的習慣用法都能在你寫代碼時幫到你。但是 John O'Hanley 指出,一些流傳極廣的 Java 習慣用法對於代碼的可維護性是有害的。如果你準備好打破習慣並優化你代碼的可維護性,那麼請繼續讀下去。 

編碼規約的出現是爲了使程序員的生活更容易 —— 特別是對經常閱讀別人代碼的程序維護人員來說。編碼規約基本上就是向這類維護人員展現“同情”的。你選擇的規約應使他們能儘可能快,儘可能沒有痛苦地理解你所寫的代碼。你越是爲了可維護性而優化你的代碼風格,你的代碼就越顯得“慈悲”,它們被理解的速度就會越快。 

相似地,較高層次的習慣用法(比如說不變體和包結構)的出現是爲了改善設計和使代碼更易讀。事實上,有些人可能會說,“改善設計”和“使代碼更易讀”最終是一回事。 

在這篇文章裏,你會看到有些很受歡迎的習慣用法應該被改變。因爲有些方法對閱讀代碼的人來說更慈悲。也許有的人會爭論說已經廣泛流行的習慣是不應該被拋棄的。道理很簡單,就因爲讀者已經習慣它們了。他們讀代碼的時候在會希望這種風格的代碼出現。然而,讀者的期望只是等式的一邊。它不應該把所有其它的考慮都掩蓋掉。最基本的問題不應該是“讀者希望讀到這樣的代碼嗎?”,而是“一個人能多快地讀懂這段代碼?”。我會輪流闡述四個有問題而又常見的習慣用法。 

一、局部變量、方法參數和域:哪個是哪個? 
(譯註:這裏域指類變量,原文使用 field 一詞) 

當試圖理解一段代碼的時候,讀者經常會問這樣一個簡單的問題:這個數據是從哪兒來的?特別是當理解一個方法時,讀者需要知道哪個項是局部變量,哪個是域,哪個是方法參數。爲了向讀者展示你的同情心,也爲了儘可能減少理解你代碼所花的精力,最好是用一個簡單的命名規則來區分這三個情況。 

很多組織在域前冠以“this”來把它們和其它的變量區分開來。不錯,但是還不夠:還應該有一個規約來把方法參數也區分開來。 

列表 1 展示了一個對這三種變量不加任何區分的 equals() 方法。 

列表 1. 一個對這三種變量不加任何區分的 equals() 方法 
Java代碼 
public boolean equals (Object arg) {  
  if (! (arg instanceof Range)) return false;  
  Range other = (Range) arg;  
  return start.equals(other.start) && end.equals(other.end);  
}  

法國作家 Marcel Proust 因其在微觀層次上研究人類體驗而出名。一個現代的 Proust 應該如何來描述這個 equals() 方法?當你遇到 start 時,你會有一點不舒服。因爲它就這麼突然間跑出來了。當看到陌生的東西時,你的大腦會被狠狠地顛一下 —— 啊噢,這是什麼東西?在通過一段煩人的篩選後(篩選的時間和方法的長度成正比),你終於發現 start 其實就是一個域。同樣麻煩的過程也會在把方法參數和其它類型的數據區分開時發生。 

列表 1 的代碼不是很長。如果方法非常大,那麼爲了理解它而花費的精力可就會很多了。 

不管多少微小,爲什麼讓讀者顛簸這麼一下,經歷這麼一個困惑的時刻?爲什麼不去掉讓你的讀者不愉快的經歷?爲什麼強迫他們自已去搞明白?如果一個簡單的命名規範可以讓讀者在潛意識裏就區分出這三種變量的話,有什麼理由不去使用呢?就像 Steve McConnell 在《代碼大全》裏說的一樣,“費勁兒搞明白兇殺案是可以的,但不應該這麼來對待代碼。你應該能讀懂它。” 

有的人也許會爭論說,這就是爲什麼域總是應該在類的開頭被聲明。但是這並不是一個好的解決方案。因爲它強迫讀者去記住域的名字或者在一大段類的代碼中玩查詢。不管哪種情況都是讓讀者做更多的活。全部的焦點應該放在讓讀你代碼的人用最少的精力來理解你的代碼。 

列表 2 是將列表 1 重寫了的代碼。它使用這樣一個命名規範: 

方法參數使用前綴 a
域使用前綴 f
局部變量不使用前綴
列表 2. 變量類型現在被清晰地區分開了 
Java代碼 
public boolean equals (Object aOther) {  
  if (! (aOther instanceof Range)) return false;  
  Range other = (Range) aOther;  
  return fStart.equals(other.fStart) && fEnd.equals(other.fEnd);  
}  

你可能會反對列表 2 所展現的代碼風格,抗議說“匈牙利命名法已經被棄用了”。但是樣的抗議是有問題的:匈牙利命名法指明類型信息。上面的命名規範跟類型沒有關係。它是用來區分域、參數和局部變量的。它們是兩個完全不同的概念。 

使用這樣的命名規範也許看起來不太重要。但相反,當這樣的規範被從頭到尾應用到你的代碼的時候,理解代碼的精力花費就會顯著地減少。因爲你可以做更多的事而不用去想它們。 

引用
把大腦從不必要的工作中解脫出來,一套好的符號表示系統能讓人們把更多的精力放在更高級的問題上,並且有效地降低精力的精耗。在阿拉伯數字表示法被引入以前,乘法是很難的。甚至整數的除法被認爲是數學的最高技巧。大概現代世界最能讓一個古希臘數學家震驚的是……西歐的一大部分人都能做任意大的數的除法。這個事實在他看起來會是完全不可能的。現在,我們可以很輕鬆地做小數的乘方。這樣一個奇蹟要歸功於一個完美符號系統的逐步發掘。” 

—— Alfred North Whitehead, An Introduction to Mathematics 

二、按層次分包:阻止包私有範圍的使用 

一個普遍的分割應用程序的方法是按層次分包: 

com.blah.action
com.blah.dao
com.blah.model
com.blah.util
換一種方式說,把屬於某一功能或特性的項分佈於各個不同的包裏就是所謂的行爲式分類法。因爲同一功能下的項之間需要互相可見,這就暗示着在這樣一程序下幾乎所有的類都是公共類。 

事實上,這種普遍的劃分包的風格把“包私有範圍”給甩出窗外了。包私有範圍不只是被簡單地忽略了,你事實上被禁止使用了。現在,包私有範圍被 Java 語言的設計者選擇爲默認的範圍。之所以做出這個選擇,當然,是爲了輕輕地把你往好的設計方向上推 —— 以最小的範圍開始,只有當必要時才增大它。(這就是通常所說的“保守祕密,以防漣漪效應”的技術。它是面向對象編程的核心)因爲一些非常奇怪的原因,社區中可觀的一部分採用了按層次分包的方法,拒絕最小化範圍的方式。這看起來是不正當的。 

一種替換方案是按功能分包: 

com.blah.painting
com.blah.buyer
com.blah.seller
com.blah.auction
com.blah.webmaster
com.blah.useraccess
com.blah.util
這裏,各個項不是按它們的行爲分組的。各個類的行爲被看作是一種實現細節。類是按最高的抽象層次來劃分的 —— 抽象的層次。這裏所有與某一功能相關聯(並且只與它關聯)的項都放在同一個包裏。這不是一種革命式的,或者說異端的想法。這正是當初創建包這個機制的初衷。 

舉例來說,在一個 Web 應用程序中,com.blah.painting 包可能會包含下面這些項: 

Painting.java:一個模型對象
PaintingDAO.java:一個數據訪問對象
PaintingAction.java:一個控制器對象
statements.sql:DAO 對象所使用的 SQL 語句
view.jsp:渲染結果給用戶看的 JSP 頁面
注意這點很重要,在這種方式中,每個包應該包含與某一功能相關的(並且只與它相關的)所有項。特別地,一個包可以包含不只是 Java 源代碼的文件。在按功能分包中,理想的狀態是可以通過刪除測試:你應該能通過刪除一個文件夾的方式來刪除某一個功能,而不會留下任何垃圾。 

這樣劃分的好處較之按層次劃分來說是非常明顯的: 

包有更高的內聚性和模塊性。包之間的耦合變得最小化。
代碼更加自我註釋化。閱讀代碼的人看看包的名字就知道功能都有什麼。在《代碼大全》中,Steve McConnell 把自注釋稱爲“易讀性的聖盃”。
這種設計仍然實踐分層的想法。可以在每一個功能裏使用單獨的類。
相關的項在一個地方。不再有必要在代碼樹裏來回找相關的項了。
每個項默認是包私有的,而且也應該是包私有的。如果一個項需要對其它包公開,那麼把它改成“public”。(注意,把一個類改成公開的,並不意味着它所有的成員都應該是公開的。在同一個類中可以混合公開和包私有成員。)
刪除一個功能就跟刪除一個目錄這麼簡單。
每一個包內包含的項更少了。整個包結構組織得更自然了。比如,一個包太大了,那麼可以將其重構成兩個或更多的包。而在按層劃分包的方式中,就根本不存在這樣的可能:它的包包含任意多的類,而且你沒法容易地重構它的包結構。
有些框架提倡按層劃分的方式。因爲傳統的包命名很常見,程序員知道哪裏可以找到自己要的東西。但是爲什麼強迫他們去找?用按功能劃分的方式,這樣煩人的尋找通常就不需要了。所以它完全優於任何這樣的命名規範。 

引用
一個區分好設計和壞設計的最重要的因素是,一個模塊能把內部數據和實現細節藏得多深。 

-- Joshua Bloch, Effective Java 

三、JavaBeans:當不變體可以勝任的時候,爲什麼還用它們? 

不變體對象是一種在創建後不改變狀態(另一種說法,數據)的對象。Scala 的主要創始人 Martin Odersky 最近讚揚了不變體的優點。在《Effective Java》中,Joshua bloch 給出了一個有力支持不變體的情況。概述 Bloch 的觀點,不變體是: 

簡單的
線程安全的,不需要同步
能自由共享
不需要用防禦式拷貝來複制它
不需要複製構建函數或者 clone() 方法
是其它類很好的基石
是不錯的 Map 鍵和 Set 元素
有原子性 —— 就是說,當有錯誤發生時,它們不會處於一種不一致或者無效狀態
Bloch 說,“除非有一個非常好的理由,否則類應該被設計成不變體。”但是它的建議好像被大面積忽略了。大多數用 JavaBeans 而不是不變體。JavaBeans 明顯比不變體要複雜。它們的複雜性來源於很廣的狀態區間。通俗點說,你可以把一個 JavaBean 想成是一個不變體的反面:它允許最大程度上的可變性。 

現在用 JavaBeans 來模型化數據庫記錄是很常見的。有沒有一種更合適的設計?不妨這樣想:如果你正在爲從數據庫的數據集裏取出一條記錄建立模型,沒有任何偏見,或者帶這樣限制的框架,你會怎樣設計?它是像一個 JavaBean 呢,還是遠遠不一樣? 

我想它會完全不一樣: 

它很可能不會包含無參構建函數。因爲這樣的構建函數不帶任何數據。一個爲數據庫的數據集建立的模型不帶任何數據,哪怕是臨時數據,這樣合理嗎?不,它不合理。一個無參的構建函數能讓你自然聯想到它的模型嗎?不,它不會。(數據確實經常是可選的。但是有多經常一個記錄所有的列都是可選的?)
它不會需要事件和監聽器。
它不會強迫你用可變體。特別是在一個 Web 應用程序中,模型對象通常只存在於一個請求的時間段裏。這樣的對象的生命週期不是很長,所以通常不需要根據用戶動作來改變狀態。
它應該會定義一個數據校驗機制。這樣的機制是數據庫應用程序中最重要的功能之一,應該被模型直接支持。(記住你所學到的關於對象的第一件事:一個對象封裝數據和針對數據的操作。在本情況下,操作就是校驗。)
校驗機制應該允許向用戶報告錯誤。
JavaBeans 規範是爲一種非常特殊的問題域而創建的:設計時操作圖形化小部件。規範裏一點沒提到數據庫。因爲它從一開始就不是爲了數據庫而建立的。結果是,發現 JavaBeans 並不是一個非常自然地爲數據記錄建模的工具就很正常了。 

從現實的角度看,很多廣泛使用的框架要求應用程序的程序員使用 JavaBeans(或者什麼相似的東西)來爲數據記錄建模。這是非常不幸的。因爲它不允許程序員利用不變體很多非常有用的東西。 

四、私有成員:爲什麼把它們放在前面? 

老的好萊塢電影總是以致謝開頭 —— 所有的致謝。相似地,大多數 Java 類把實現細節(私有成員)放在前頭。列表 3 展示了一下這種風格的典型例子。 

列表3. 放在前面的私有成員 
Java代碼 
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;  
   }  
     
  //..省略  
}  

然而,把私有成員放在最後,而不是最前,看上去對於閱讀者來說是更慈悲的。當試圖去理解某事 —— 任何事 —— 你應該以從普遍到特殊的方式來看。更明確地說,你應該從高層抽象到低層抽象。如果反着來,那麼從一開始可能就不會把握住整體要點,也可能把握不住各個部件是怎麼協同工作的。最終導致迷失在一大堆可能並沒有什麼聯繫的點裏。 

抽象的關鍵就是忽略細節。抽象的層次越高,細節被忽略得越多。讀者忽略的細節越多他們越高興。把很多細節記在腦子裏是很痛苦的。所以,細節越少越好。這樣,把私有成員放在最後看起來對讀者就更具同情心。因爲它把與讀者現有任務(不管是什麼任務)無關緊要的細節給剔除了。 

原來在 C++ 編程文化裏,私有成員是放在前面的。就跟現在的 Java 一樣。然而,不像 Java 社區,C++ 社區很快地認識到這是一個沒有任何幫助的習慣。所以它現在被反過來了。這裏是一段來自《A Typical C++ Style Guide》的話: 
引用
注意,把公共接口放在類中的最前面,然後是保護成員,最後是私有成員。原因有下: 

程序員更關心類的接口而不是實現
當程序員需要使用一個類的時候,他們需要接口而不是實現

把接口放在前面看起來很有道理。把實現、私有部分放在前面是由於歷史原因。因爲最初的樣板都使用私有成員前置方法。隨着時間的推移,重點已經發生了轉移。 

類似地,《Imperical College London C++ Style Guide》說,“把公共區域放在前面,所有用戶感興趣的東西就都被集中放置在了類定義的前面。保護區域也許會讓考慮繼承它的設計者感興趣。但私有區域包含的細節應該是對人最沒有吸引力的。” 

當讀者要理解一個類的時候,應該通過 Javadoc 生成的文檔,而不是源代碼來看。然而, Javadoc 生成的文檔不包含實現細節。當維護一個類的時候,程序顯然還是須要訪問源代碼。 

對於任何類型的技術文檔來說,把高層的信息放在開頭是很正常的 —— 比如說一本書的目錄,或者是一篇文學論文的摘要。爲什麼 Java 類要反着做?把私有成員放在前面似乎是一個應該被打破的壞習慣。它一開始之所以存在很可能是因爲受早期代碼風格地影響。比如說 Sun 公佈的編程規約。就像 Joshua Bloch 所說,“我不會太把那份文檔(Sun 的編程規約)當回事。Sun 並不積極維護和使用它。” 

仿照 Javadoc 生成的文檔風格來編排代碼看起來更合理:先是構造函數,然後是非私有方法,最後是私有域和方法。這是唯一可以讓讀者的思路從高層抽象到低層抽象自然轉移的風格。 

總結 

我已經論證了這裏面提到的四種應該被打破的 Java 習慣。評判某一改變是否合理的終級標準是它是否讓可以讓代碼明顯地更易讀、理解和使用,並且在這個過程中,它是否會讓讀者的精神體驗更愉悅。不變體和分包風格的例子也應該會刺激你朝改善設計的方向邁進。 

總之,我建議下面的習慣應該被推薦的習慣所代替: 

使用一種命名規範來區分:局部變量、域和方法參數。
優先使用按功能分包法而不是按層次分包法。
優先使用不變體而不是 JavaBeans。
把類中的項按作用範圍從高到低排列。也就是說,把私有成員放到最後。
如果這篇文章所說的事是正確的,那麼引出一個問題:這麼多年過去後,爲什麼這樣有害的習慣會在 Java 社區文化中延續到今天?爲了回答這個問題,我提出下面的推測。早期的風格指導和其它工具發佈的時候,大家還不是很有經驗。他們把大多數事都做對了,但是還是有極小一部分錯了。這是非常正常的,並不能說明它們的作者很糟糕。這其中 Sun 公佈的一些指導和例子被廣泛模仿。它們有着非常強的影響力,並且經常被當作是權威。也許它們被過分看重了。 

有些有害的習慣在 Java 的早期穩固下來是因爲它們已經變成了非常流行的、根深地固的習慣 —— 也許有點輿論催眠或者說羣體思維的味道。然而正是流行和權威經常會把所有的相反看法給掩蓋掉。 

Java 編程文化會逐漸去掉這些壞習慣嗎?還是儘管知道它們的壞處還依然如故?我的猜測是,不幸地,它們還會在 Java 編程文化中佔有一席之地。但是我很希望我是錯的! 

作者傳 

John O'Hanley 是 javapractices.com 的創始者,並且是 WEB4J 框架的作者。他有着 10 年的編程經驗。現居住於加拿大愛德華王子島。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章