關於代碼的質量(重構)

其實在自己大量編碼以後.經驗告訴我們一個大的方法讓別人看起來頭疼,維護相當困難.,所以我們都把大的方法體細化成小的邏輯單元(小的方法),這樣別人看起來簡單易懂,基本不需要註釋的.

當然最重要的是重構代碼,可以讓代碼重用.如果別的類需要該類對象的一個方法,但是你如果寫的該類是隻用一個大方法.,裏面包含了使用類需要的方法邏輯...那麼我們就不可能直接使用類的對象.然後調用方法..而是自己又重新寫一篇代碼.如果你把大方法寫成很多小方法...那麼可以重用的機會就很多了...尤其是在公用類的設計上...更要細分方法...

文介紹了從另外一種不同的途徑使重用成爲可能的三個步驟。

第一步:將功能實現從類實例的方法中移出<o:p></o:p>

由於缺乏精確性,類繼承不是非常理想的代碼重用機制。換句話說,如果不繼承一個類的數據成員和其他的方法,那麼你就無法重用這個類的某個單獨的方法。這些額外的不必要的負擔使方法重用的代碼變得複雜。派生類對其父類的依賴性也以入了額外的複雜性:對父類的改動會對子類造成影響;當修改任意一個類的時候,我們很難記得清哪個方法被覆蓋,哪個沒有;而且被覆蓋的方法是否會調用父類中相應的方法並不非常清晰地顯現。

任何執行單一概念任務的方法應該能夠成爲代碼用的首選而獨立存在。爲了達到這個目標,我們必須會到過程化的編程模式,將代碼從類實例的方法中移出,形成具有全局可見性的過程。爲了提高這種過程的可重用性,過程代碼應該象靜態的通用方法一樣編寫:每個過程只能使用自己的輸入參數,只能調用其他全局性的過程完成其工作,不能使用任何非本地的變量。這種對外部依賴的簡化降低了過程使用的複雜性,也增加了在其他地方使用此過程的可能性。當然,由於其結構通常會變得更爲清晰,即使拋開重用的目的不談我們也可以從這種代碼的組織方式中受益。

Java中,方法不能脫離類而單獨存在。因此,你可以對相關的過程進行組織並使它們成爲一個獨立的類中的公共靜態方法。例如,對於如下所示的一個類:

class Polygon{<o:p></o:p>

…<o:p></o:p>

public int getPerimeter(){…}<o:p></o:p>

public boolean isConvex(){…}<o:p></o:p>

public Boolean containsPoint(Point p){…}<o:p></o:p>

…<o:p></o:p>

}<o:p></o:p>

可以將它改寫成下面的形式:

class Polygon { <o:p></o:p>

…<o:p></o:p>

public int getPerimeter() { return pPolygon.computePerimeter(this);} <o:p></o:p>

public boolean isConvex() { return pPolygon.isConvex(this);} <o:p></o:p>

public boolean containsPoint() { return pPolygon.containsPoint(this, p);} <o:p></o:p>

…<o:p></o:p>

} <o:p></o:p>

在此處,nPolygon應該是這個樣子:

class pPolygon { <o:p></o:p>

static public int computePerimeter(Polygon polygon) {...} <o:p></o:p>

static public boolean isConvex(Polygon polygon) {...} <o:p></o:p>

static public boolean containsPoint(Polygon polygon, Point p) {...} <o:p></o:p>

<o:p></o:p>

從類的名字pPolygon可以看出,該類所封裝的過程主要與Polygon類型的對象有關。名字前面的p表示該類的唯一目的是組織公共靜態過程。在Java中,類的名字以小寫字母開頭不是一種標準的做法,但象pPloygon這樣的類事實上並不執行普通類的功能。也就是說,它並不代表着一類對象,它只是語言本身所需要的用於代碼組織的實體。

 在上面這個例子中,改動代碼的總體影響是使得客戶代碼不必爲了重用其功能而從Polygon繼承。Polygon類的功能現在已經由pPolygon類以過程爲單位提供。客戶代碼只使用自己需要的代碼,無需關心自身並不需要的功能。

這並不意味着在這種新型的過程化編程模式中,類不服務於更有用的目的。恰恰相反,類執行組織和封裝對象數據成員的必要工作。而且它們通過多重接口實現多態性的能力也爲代碼重用提供了顯著的支持,這將在下一個步驟中討論。然而,由於將功能實現包含在實例方法中無法實現理想的代碼重用,所以通過類繼承實現代碼重用和多態性支持也不應成爲最佳的技術選擇。

在一本被廣爲閱讀的書《Design Patterns》中曾簡要地提及一種略有不同的技術。策略模式(Strategy Pattern)提倡將相關算法的每個成員封裝在一個通用的接口下,以便於客戶端代碼可交換地使用其算法。由於一個算法通常被作爲一個或幾個獨立的過程進行編碼,這種封裝更注重執行單獨任務的過程的重用,而不是執行多種任務的、包含代碼和數據的對象的重用。這一步驟體現了相同的基本思想。

然而,將一個算法封裝在一個接口下意味着將算法作爲實現接口的對象進行編碼。這意味着我們仍然依賴於一個與所包裝的對象的數據和其他方法相耦合的過程,這樣便會使其複用變得複雜。此外還存在這樣一個問題,每次需要使用這個算法的時候都必須實例化這些對象,這便會降低程序的性能。值得慶幸的是,設計模式提供了針對這兩個問題的解決方法。可以在對策略對象進行編碼時應用享元模式(Flyweight Pattern,譯者注:還存在一種譯法爲輕量模式),這樣每個對象只會存在一個被共知共享的實例(這針對程序性能的問題),而且每個共享對象在訪問間隔中並不維持狀態(於是對象將沒有數據成員,這針對大多數的耦合問題)。由此產生的享元--策略模式非常類似於在這一步驟中所提到的將功能實現封裝在全局可見的、無狀態的過程中的技術。(譯者注:以上這兩段文字讀起來可能有些晦澀難解,建議有興趣的讀者參閱文中所提到《設計模式》一書,Erich Gamma等著、李英軍等譯、機械工業出版社出版。)

第二步:將非原始的輸入參數類型改爲接口類型<o:p></o:p>

在面向對象編程中,代碼重用的真正基礎在於通過接口參數類型利用多態性,而不是通過類繼承,正如Allen Holub “Build User Interfaces for Object-Oriented System, Part 2”中所述:

……你應該通過對接口而不是類編程實現重用。如果一個方法的所有參數都是某個已知接口的引用,這個接口由一些你所不知道的類實現,那麼這個方法就能夠操作這樣一些對象:當編寫方法的代碼時,這些對象的類甚至還不存在。從技術上講,可重用的是方法,而不是傳遞給方法的對象。”

Holub所講的方法應用於第一步所得到的結果,只要某塊功能代碼能夠作爲一個全局可見的過程而獨立存在,你就可以將其每個類類型(class-type)的輸入參數改爲一個接口類型,這樣便能進一步提高其重用的潛力。那麼,實現此接口類型的任何類的對象都可以作爲參數使用,而不僅僅侷限於原始類。由此,這個過程對可能存在的大量的對象類型都成爲可用的。

例如,有這樣一個全局可見的靜態過程

static public boolean contains(Rectangle rect, int x, int y) {…}<o:p></o:p>

這個方法用於檢查給定的矩形是否包含某個給定的點。在這個例子中,rect參數的類型可以從Rectangle類改變爲接口類型,如下所示:

static public boolean contains(Rectangular rect, int x, int y){…}<o:p></o:p>

Rectangular可以是下面形式的接口:

public interface Rectangular{<o:p></o:p>

       Rectangle getBounds();<o:p></o:p>

}<o:p></o:p>

現在,所有可以被描述爲矩形的類(也就意味着實現了Rectangular接口)的對象都可以作爲傳遞給pRectangular.contains()rect參數。通過放寬所傳遞的參數類型的限制,我們使方法具有更好的可重用性。

不過,在上面這個例子中,Rectangular接口的getBounds方法返回一個Rectangle類型,你可能會懷疑使用這個接口是否具有真正的價值;換句話說,如果我們知道傳入過程的對象會在被調用時返回一個Rectangle,爲什麼不直接傳入Rectangle取代接口類型呢?不這樣做的最重要原因與集合有關,假設有這樣一個方法:

static public boolean areAnyOverlapping(Collection rects) {…}<o:p></o:p>

這個方法的目的在於檢查給定集合中的任意矩形對象是否存在重疊。那麼,在方法內部遍歷集合中的每個對象時,如果無法將對象造型(cast)成如Rectangular這樣的接口類型,那麼將如何能夠訪問對象的矩形區域呢?唯一的選擇是將對象造型成爲其特定的類型(我們直到它有一個能夠返回rectangle的方法),這意味着方法必須事先知道其所要操作的是什麼類型。這恰恰是這一步驟力圖首先要避免的問題!

第三步:選擇低耦合的輸入參數接口類型<o:p></o:p>

完成第二步之後,應該選擇什麼樣的接口類型來取代給定的類型呢?答案是能夠通過參數完全描述過程的需求,同時又具有最少的額外負擔的接口類型。參數對象所要實現的接口越簡單,其他特定類實現此接口的機會就越大——由此,其對象可以作爲參數使用的類也就越多。通過下面的例子可以很容易地看到這點:

static public boolean areOverlapping(Window window1, Window window2) {...}<o:p></o:p>

這個方法用於檢查兩個窗口(假定是矩形窗口)是否重疊,如果這個方法只要求從參數獲得兩個窗口的矩形座標,那麼簡化參數的類型使其能反映這個事實是一種更好的選擇:

static public boolean areOverlapping(Rectangular rect1, Rectangular rect2) {...}<o:p></o:p>

以上的代碼假設先前的Window類型的對象同樣可以實現Rectangular接口。現在對於所有的矩形對象,都可以重用第一個方法所包含的功能了。

你可能多次體驗到當一個接口能夠完全確定需要通過參數獲哪那些內容時,會存在太多不必要的方法。在這種情況下,應該在全局命名空間中定義一個新的公共接口以供其他可能面臨同一困境的方法重用。

你可能還會不止一次地發現,在確定需要通過單一過程的一個參數獲取哪些內容時,最好創建一個單獨的接口。你應該只爲這個參數使用此接口。這通常會在你希望如同C語言中的函數指針一樣使用參數的情況下出現。例如下面的過程:

static public void sort(List list, SortComparison comp) {...}<o:p></o:p>

此過程使用參數所提供的比較對象comp,通過比較給定列表中的所有對象而對其進行排序,sortcomp的全部要求是調用一個單獨的方法進行比較。因此,SortComparison應該是隻帶有一個方法的接口:

public interface SortComparison { <o:p></o:p>

boolean comesBefore(Object a, Object b); <o:p></o:p>

} <o:p></o:p>

這個接口的唯一目的是爲sort提供一個與其完成任務所需功能相聯繫的鉤子(hook),因此SortComparison無法在其他地方重用。

結束語<o:p></o:p>

以上所述的三個步驟用於現有的、按照相對傳統的面向對象方法所編寫的代碼。這些步驟與面向對象編程技術結合就形成了一種可以運用於今後代碼編寫中的新方法,它可以提高代碼的可重用性和內聚性,同時降低了耦合度及複雜性。

很顯然,這些步驟無法運用於那些在本質上就不適合於重用的代碼。這類代碼通常出現在應用程序的表示層(presentation layer)。例如程序中用於創建用戶界面的代碼,以及將輸入事件與完成實際工作的過程相聯繫的控制代碼,都是屬於那種其功能在不同的程序中差別很大的代碼,這種代碼的重用幾乎是不可能的。

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