對程式設計語言而言,好的編碼風格不僅能在程式編寫初期產生有效架構編碼,還可以讓我們的編碼更加清晰規範。但是,正如本文作者所說,一些Java程式的編碼風格雖應用廣泛,卻會對編碼的可維護性產生負面影響,對我們的編程有害。本文告訴你如何打破這種風格,重寫這4個有害的編碼風格,最佳化編碼,提高可維護性。
程式中的編碼風格讓我們的編程工作變得輕鬆,特別是程式維護員,他們要經常閱讀其他人編寫的程式編碼,這一點尤其突出。編碼規範從根本上解決了程式維護員的難題;規範的編碼閱讀和理解起來更容易,也可以快速的不費力氣的借鑒別人的編碼。對將來維護你編碼的人來說,你的編碼越最佳化,他們就越喜歡你的編碼,理解起來也就越快。
同樣,高水平的編碼風格(例如固定的封閉結構)目的在於改善設計和使編碼更易於理解。事實上,最後有些人會認為改善設計和提高編碼的易讀性是一回事。
本文中你會看到一些流行的編碼風格被面向讀者的更易於接受的風格所替代。有人爭論說這些風格都已經被大家廣泛使用,不應該簡單的為了達到讀者的期望而拋棄。然而,讀者的期待只是其中一方面的原因,不可能淩駕於所有因素之上。列出四種常見的問題:
1.對局域變數(local variables)、參數(method arguments)、欄位(fields)這三種變數的命名沒有區分:
對看編碼的人來說,首先要弄清這些資料如何定義的?看一個類時,得弄清楚每個條目是局域變數?欄位?還是參數?有必要使用一個簡單的命名規範來定義這些變數,增加易讀性。
很多權威機構規範過欄位變數用以區分它與其它的變數,但這遠遠不夠。可以把對欄位的合理的命名規範邏輯也應用在參數上面。先看樣本1:沒有進行區分這三種變數的類定義,如下所示:
樣本1:
1 public boolean equals (Object arg) {2 if (! (arg instanceof Range)) return false;3 Range other = (Range) arg;4 return start.equals(other.start) && end.equals(other.end);5 }
在這個方法中,arg直接用argument的縮寫,雖然大家一看就知道這是參數了,但這種命名方式卻丟失了參數代表的對象本身 的含義。大家知道這 是參數,卻不知道這是什麼參數。如果方法的參數多一點,都按照arg1,arg2這樣的方式命名,閱讀代碼 的時候很頭疼。另外兩個欄位變數,start和 end,突然憑空而出,想一下才知道這應該是欄位。當然,這個方法很短,造成的困難還不大,如果這個方法比較長的話,突然看到start和end兩個變 量,一般會先在前面找一下是不是局部變數,然後才能確定是類的欄位變數。
這個問題貌似微不足道,但為什麼要讓代碼閱讀者花費額外時間在這些瑣碎的問題上呢?如果有個方案能讓代碼閱讀者一目瞭然的明白變數是那種變數,為什 麼不採用呢?就如同Steve McConnell在 《代碼大全》中說的:"讓人費神去琢磨神秘殺人兇手這沒有問題,但你不需要琢磨程式碼,代碼是用來閱讀的。
接下來看樣本2,使用命名規範後對樣本1重寫以後的代碼,用到的命名規範有:
參數定義時名字加首碼a
欄位定義時名字加首碼f
局域變數定義時不加任何首碼
樣本2:對變數類型進行區分
1 public boolean equals (Object aOther) {2 if (! (aOther instanceof Range)) return false;3 Range other = (Range) aOther;4 return fStart.equals(other.fStart) && fEnd.equals(other.fEnd);5 }
你可能反對樣本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)成員是可以在同一個類裡共存的。)
- 刪除一個功能或特徵只需要簡單的刪除一個檔案夾。
- 每個包內一般只有很少的成員,這樣包可以很自然的按照進化式發展。如果包慢慢變的太大,就可以再進行細分,把它重構為兩個或者更多新的包,類似於物種進化。而按照層次劃分的方式,就沒辦法進化式發展,重構也不容易。
一些架構推薦使用層層定義包的傳統的方式做為包的命名方法:由於使用傳統的包命名,開發人員總能知道在哪個位置可以找到
這些項目,但是為什麼避免人們這樣做呢?使用另一種按特徵定義包的風格,就不需要這種單調的操縱,因此,按特徵定義完
全超越了任何其它命名規範。約書亞布洛赫在《高效的Java》一書中說到:區分一個設計好壞的唯一重要因素是模組內部隱藏
的資料和其它模組中涉及的實現過程的程度。
3.習慣用JavaBeans而不是不可變對象
不可變對象是構造後狀態不改變。Scala的主要創造者Martin Odersky最近還稱讚過這種不可變對象。在《高效的Java》一書
中,Joshua Bloch列舉了大量執行個體支援使用不可變對象,並總結了很多優點。但他的意見,似乎很大程度上被忽略。大多數程
序使用JavaBeans來替代不可變對象。JavaBean明顯要比不可變對象複雜的多,因為它的巨大的聲明空間。粗略的講,你可以
把JavaBean看作是與不可變對象完全相反的對象:它允許最大的可變性。
JavaBean常被用來做資料庫記錄的映射。假如你要從資料庫記錄集映射一行為對象,不考慮現有的持久化方案和架構,你會將
這個對象設計成什麼樣子?跟javabean相似呢還是完全不一樣?
我認為會完全不一樣,說明如下:
- 它不包含一個無參數構造方法(這一特徵是javabean必備的。)。作者認為一個資料庫記錄的對象如果不包含任何資料是沒有意義的。一個資料庫表的所有欄位都是可選的情況有多少?
It would likely not have anything to say about events and listeners.(不太明白作者的意思)
- 它不強迫你用可變的對象。
它內部有一個資料驗證機制。這樣一個驗證機制對大多數資料庫應用非常重要。(記住對象的第一原則:一個對象應該同時封裝資料和對資料的操作。在這種情況下,操作就是驗證資料。)
資料驗證機制可以給終端使用者(end user)報錯。
按照javabeans的說明,javabeans是用來解決特殊領域的問題:在圖形介面程式的設計中充當小組件。說明中絕對沒有提到資料庫。但現在通常用javabean來做資料庫記錄的映射。從實際角度來講,許多被廣泛使用的架構要求應用程式使用JavaBeans(或者其它類似的規範)來映射資料庫記錄。這種濫用不利於編程者瞭解和使用不可變對象。
4.私人成員排在其它成員的前面
類成員的排序沒有按照成員的範圍的大小排列,而是把private放在前面。
以前的好萊塢影片開頭總是長篇的榮譽。同樣地,大多數Java類把私人成員放在最前面。樣本3給出這種風格的典型例子:
1 public class OilWell implements EnergySource { 2 private Long id; 3 private String name; 4 private String location; 5 private Date discoveryDate; 6 private Long totalReserves; 7 private Long productionToDate; 8 9 public Long getId() {10 return id;11 }12 public void setId(Long id) {13 this.id = id;14 }15 16 //..elided17 }
然而,如果把私人成員定義放在後面,讀者閱讀會更容易。因為人們認識一個事物的通常過程都是從一般到特殊,從抽象層次來說,是從高層次到低層次的認識過程。如果你倒過來的話,讀者就不能從整體上把握事物,也不能抓住事物的本質,只能在一堆具體的片段中迷失。
整體的抽象讓你忽略了細節。抽象的層次越高,你可以忽略越多的細節。讀者閱讀一個類時可以忽略的細節越多他會越高興。腦袋裡填充太多的細節是痛苦的,所以細節越少越好。因此,將私人成員放在最後會顯得更富有同情心,因為這樣阻止了不必要的細節顯露給讀者。
本來C++程式的習慣也是像Java一樣把private成員放在最開始。然而,C++社區迅速的認識到這是一個有害的規範,這個規範現在已經被修正。這裡給出一個經典的C++風格指南裡的注釋:
同樣,倫敦大學帝國學院關於C++的指面中也說到:把公有的部分放在前面,讀者會更感興趣閱讀,然後是保護的部分,最後是私人的部分。
有人會持反對意見,認為讀者可以使用程式文檔來理解類,而不是直接看原始碼。這種理由似乎不成立,因為程式文檔中沒有相關的實現細節,這時看原始碼是很有必要的。
所有的技術文檔,通常都把難理解的資訊放在開頭,比如抽象的學術論文。為什麼Java不打破這種常規呢?把私人成員放在最開頭部分看起來是不是打破常規的好習慣。這種習慣似乎是sun早期的編碼規範造成的。
將代碼按照javadoc的順序編排是非常好的:首先是構造方法,然後是非私人方法,最後是私人部分和方法。這樣讀者閱讀的時候很自然的從抽象層次的高向低運動。
本文所講的是一些Java的不好習慣和風格需要改變。最終的目地是希望我們的代碼易讀性更強,讓讀者更易於理解。