四個有害的Java編碼習慣

程序中的編碼風格讓我們的編程工作變得輕鬆,特別是程序維護員,他們要經常閱讀其他人編寫的程序編碼,這一點尤其突出。編碼規範從根本上解決了程序維護員的難題;規範的編碼閱讀和理解起來更容易,也可以快速的不費力氣的借鑑別人的編碼。對將來維護你編碼的人來說,你的編碼越優化,他們就越喜歡你的編碼,理解起來也就越快。

同樣,高水平的編碼風格(例如固定的封閉結構)目的在於改善設計和使編碼更易於理解。事實上,最後有些人會認爲改善設計和提高編碼的易讀性是一回事。

本文中你會看到一些流行的編碼風格被面向讀者的更易於接受的風格所替代。有人爭論說這些風格都已經被大家廣泛使用,不應該簡單的爲了達到讀者的期望而拋棄。然而,讀者的期待只是其中一方面的原因,不可能凌駕於所有因素之上。列出四種常見的問題:

1.對局域變量(local variables)、參數(method arguments)、字段(fields)這三種變量的命名沒有區分:

對看編碼的人來說,首先要弄清這些數據如何定義的?看一個類時,得弄清楚每個條目是局域變量?字段?還是參數?有必要使用一個簡單的命名約定來定義這些變量,增加易讀性。

很多權威機構規範過字段變量用以區分它與其它的變量,但這遠遠不夠。可以把對字段的合理的命名約定邏輯也應用在參數上面。先看示例1:沒有進行區分這三種變量的類定義,如下所示:

示例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);
}

 在這個方法中,arg直接用argument的縮寫,雖然大家一看就知道這是參數了,但這種命名方式卻丟失了參數代表的對象本身 的含義。大家知道這 是參數,卻不知道這是什麼參數。如果方法的參數多一點,都按照arg1,arg2這樣的方式命名,閱讀代碼  的時候很頭疼。另外兩個字段變量,start和 end,突然憑空而出,想一下才知道這應該是字段。當然,這個方法很短,造成的困難還不大,如果這個方法比較長的話,突然看到start和end兩個變 量,一般會先在前面找一下是不是局部變量,然後才能確定是類的字段變量。

 這個問題貌似微不足道,但爲什麼要讓代碼閱讀者花費額外時間在這些瑣碎的問題上呢?如果有個方案能讓代碼閱讀者一目瞭然的明白變量是那種變量,爲什 麼不採用呢?就如同Steve McConnell在 《代碼大全》中說的:"讓人費神去琢磨神祕殺人兇手這沒有問題,但你不需要琢磨程序代碼,代碼是用來閱讀的。

接下來看示例2,使用命名約定後對示例1重寫以後的代碼,用到的命名約定有:
  • 參數定義時名字加前綴a
  • 字段定義時名字加前綴f
  • 局域變量定義時不加任何前綴
示例2:對變量類型進行區分
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中的風格,反對過時了的匈牙利符號,但是我認爲反對是錯誤的,因爲匈牙利符號能詳細說明信息的類型。
上面的命名約定區分了類型。而且這樣做分清了字段、變量和局域變量,這是兩種完全不同的概念。
這種命名約定的方式並不像看起來那麼微不足道:當這些約定用在程序編碼中時,會大大降低理解的難度,因爲你可以不需
要先分辨這些變量,省去不少時間。

2.按層次劃分包
,而不是根據特徵或功能劃分

最常見的劃分應用序就是按層次命名包:
  • com.blah.action
  • com.blah.dao
  • com.blah.model
  • com.blah.util
也就是說,把具有同樣特徵或者功能的類劃分到了不同的包裏。因爲成員的屬性對其他成員應該是可見的,這就意味着幾乎應用程序中所有的類都是公共的。實際上,這種按層次劃分包的方法完全扔掉了Java的包內私有。包內私有應該徹底不使用。現在,包內私有是Java程序語言中設計者的默認作用域。這種包的劃分習慣也違反了面向對象編程的核心原則之--儘量保持私有以減少影響,因爲這種習慣強迫你必須擴大類的作用域。由於一些奇怪的原因,一些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: 一個model對象
  • PaintingDAO.java: 一個數據存取對象Dao
  • PaintingAction.java: 一個控制或者行爲對象
  • statements.sql: Dao對象使用的SQl文件
  • view.jsp: Jsp文件
需要特別說是的是,這種劃分方法,每一個包都包含所有成員有關的特徵文件,而不僅僅是Java源文件。這種按特徵劃分包的方法,要求在做刪除操作時要注意,刪除一個特徵時要刪掉它的整個目錄,不能保存在源碼中。
這種方法優於按層次劃分包的方法,表現在以下幾點:
  • 包是高內聚的,並且模塊化,包與包之間的耦合性被降到最低。

  • 代碼的自描述性增強. 讀者只需看包的名字就對程序有些什麼功能或特徵有了大概的印象。在《代碼大全》中, Steve McConnell 將自描述性的代碼比作 "易讀的聖盃",來表達它的易讀性。

  • 把類按照每個特徵和功能區分開可以很容易實現分層設計。

  • 相關的成員在同一個位置。不需要爲了編輯一個相關的成員而去瀏覽整個源碼樹。

  • 成員的作用域默認是包內私有。只有當另外的包需要訪問某個成員的時候,才把它修改爲public. (需要注意的是修改一個類爲public,並不意味着它的所有類成員都應該改爲public。public成員和包內私有(package- private)成員是可以在同一個類裏共存的。)

  • 刪除一個功能或特徵只需要簡單的刪除一個文件夾。
  • 每個包內一般只有很少的成員,這樣包可以很自然的按照進化式發展。如果包慢慢變的太大,就可以再進行細分,把它重構爲兩個或者更多新的包,類似於物種進化。而按照層次劃分的方式,就沒辦法進化式發展,重構也不容易。
一些框架推薦使用層層定義包的傳統的方式做爲包的命名方法:由於使用傳統的包命名,開發者總能知道在哪個位置可以找到
這些項目,但是爲什麼避免人們這樣做呢?使用另一種按特徵定義包的風格,就不需要這種單調的操縱,因此,
按特徵定義完
全超越了任何其它命名約定。約書亞布洛赫在《高效的》一書中說到:區分一個設計好壞的唯一重要因素是模塊內部隱藏
的數據和其它模塊中涉及的實現過程的程度。

3.Beans而不是不可變對象

不可變對象是構造後狀態不改變。Scala的主要創造者Martin Odersky最近還稱讚過這種不可變對象。在《高效的》一書
中,Joshua Bloch列舉了大量實例支持使用不可變對象,並總結了很多優點。但他的意見,似乎很大程度上被忽略。大多數程
序使用Beans來替代不可變對象。JavaBean明顯要比不可變對象複雜的多,因爲它的巨大的聲明空間。粗略的講,你可以
Bean看作是與不可變對象完全相反的對象:它允許最大的可變性。
Bean常被用來做數據庫記錄的映射。假如你要從數據庫記錄集映射一行爲對象,不考慮現有的持久化方案和框架,你會將
這個對象設計成什麼樣子?跟javabean相似呢還是完全不一樣?

我認爲會完全不一樣,說明如下:

  • 它不包含一個無參數構造方法(這一特徵是javabean必備的。)。作者認爲一個數據庫記錄的對象如果不包含任何數據是沒有意義的。一個數據庫表的所有字段都是可選的情況有多少?
  • It would likely not have anything to say about events and listeners.(不太明白作者的意思)

  • 它不強迫你用可變的對象。
  • 它內部有一個數據驗證機制。這樣一個驗證機制對大多數數據庫應用非常重要。(記住對象的第一原則:一個對象應該同時封裝數據和對數據的操作。在這種情況下,操作就是驗證數據。)

  • 數據驗證機制可以給最終用戶(end user)報錯。

按照javabeans的說明,javabeans是用來解決特殊領域的問題:在圖形界面程序的設計中充當小部件。說明中絕對沒有提到數據庫。但現在通常用javabean來做數據庫記錄的映射。從實際角度來講,許多被廣泛使用的框架要求應用程序使用JavaBeans(或者其它類似的規範)來映射數據庫記錄。這種濫用不利於編程者瞭解和使用不可變對象。

4.私有成員排在其它成員的前面

類成員的排序沒有按照成員的作用域的大小排列,而是把private放在前面。
以前的好萊塢影片開頭總是長篇的榮譽。同樣地,大多數類把私有成員放在最前面。示例3給出這種風格的典型例子:

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
}

然而,如果把私有成員定義放在後面,讀者閱讀會更容易。因爲人們認識一個事物的通常過程都是從一般到特殊,從抽象層次來說,是從高層次到低層次的認識過程。如果你倒過來的話,讀者就不能從整體上把握事物,也不能抓住事物的本質,只能在一堆具體的片段中迷失。

整體的抽象讓你忽略了細節。抽象的層次越高,你可以忽略越多的細節。讀者閱讀一個類時可以忽略的細節越多他會越高興。腦袋裏填充太多的細節是痛苦的,所以細節越少越好。因此,將私有成員放在最後會顯得更富有同情心,因爲這樣阻止了不必要的細節顯露給讀者。

本來C++程序的習慣也是像Java一樣把private成員放在最開始。然而,C++社區迅速的認識到這是一個有害的規範,這個規範現在已經被修正。這裏給出一個經典的C++風格指南里的註釋:

注意:public 接口應該放在class的最開始,其次是protected成員,最後是private成員。原因是:

  • 程序員應該更關心接口而不是具體實現。
  • 當程序員需要用一個類的時候,他們需要的是接口而不是實現。

把接口放在開始是非常有意義的。把實現部分,私有部分,放在開始是一個歷史遺留問題。最後還是要反覆強調一下,一個類的接口的重要性超過實現細節。

 

同樣,倫敦大學帝國學院關於C++的指面中也說到:把公有的部分放在前面,讀者會更感興趣閱讀,然後是保護的部分,最後是私有的部分。

有人會持反對意見,認爲讀者可以使用程序文檔來理解類,而不是直接看源代碼。這種理由似乎不成立,因爲程序文檔中沒有相關的實現細節,這時看源代碼是很有必要的。

所有的技術文檔,通常都把難理解的信息放在開頭,比如抽象的學術論文。爲什麼Java不打破這種常規呢?把私有成員放在最開頭部分看起來是不是打破常規的好習慣。這種習慣似乎是sun早期的編碼規範造成的。

將代碼按照javadoc的順序編排是非常好的:首先是構造方法,然後是非私有方法,最後是私有部分和方法。這樣讀者閱讀的時候很自然的從抽象層次的高向低運動。

 

本文所講的是一些Java的不好習慣和風格需要改變。最終的目地是希望我們的代碼易讀性更強,讓讀者更易於理解。

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