天天看點

最大限制地提高代碼的可重用性

<script language="javascript" type="text/javascript"> showbanner(6,6,1); </script><script type="text/javascript"> google_ad_client ="pub-2141342037947367";google_ad_width = 120;google_ad_height =240;google_ad_format = "120x240_as";google_ad_channel="8570654326";google_color_border = "CCCCCC";google_color_bg ="FFFFFF";google_color_link = "000000";google_color_url ="666666";google_color_text = "333333"; </script><script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"> </script> name="google_ads_frame" marginwidth="0" marginheight="0" src="http://pagead2.googlesyndication.com/pagead/ads?client=ca-pub-2141342037947367&dt=1113128732499&lmt=1113128732&prev_fmts=468x60_pas_abgnc&format=120x240_as&output=html&channel=8570654326&url=http%3A%2F%2Fwww.javaresearch.org%2Farticle%2Fshowarticle.jsp%3Fcolumn%3D544%26thread%3D25300&color_bg=FFFFFF&color_text=333333&color_link=000000&color_url=666666&color_border=CCCCCC&ref=http%3A%2F%2Fwww.javaresearch.org%2F&u_h=768&u_w=1024&u_ah=738&u_aw=1024&u_cd=32&u_tz=480&u_java=true" frameborder="0" width="120" scrolling="no" height="240" allowtransparency="65535">

    重用是一種神話,這似乎正在日漸成為程式設計人員的一種共識。然而,重用可能難以實作,因為傳統面向對象程式設計方法在可重用性方面存在一些不足。本技巧說明了組成支援重用的一種不同方法的三個步驟。 

第一步:将功能移出類執行個體方法

由于類繼承機制缺乏精确性,是以對于代碼重用來說它并不是一種最理想的機制。也就是說,如果您要重用某個類的單個方法,就必須繼承該類的其他方法以及資料成員。這種累贅不必要地将要重用此方法的代碼複雜化了。繼承類對其父類的依賴性引入了額外的複雜性:對父類的更改會影響子類;當更改父類或子類中的任一方時,很難記住覆寫了哪些方法(或者沒有覆寫哪些方法);而且是否應該調用相應的父類方法也不明朗。

執行單一概念性任務的任何方法都應該是獨立的,并應将其作為要重用的首選方法。要實作這一點,我們必須傳回到過程式程式設計,将代碼移出類執行個體方法并将其移入全局可見的過程中。為了提高這類過程的可重用性,您應該像編寫靜态實用方法那樣編寫這類方法:每個過程隻使用其自身的輸入參數和/或對其他全局可見過程的調用完成其工作,而且不應該使用任何非局部變量。這種外部依賴性的減弱降低了使用該過程的複雜性,進而可促進在别處對它的重用。當然,即便那些不計劃重用的代碼也會從這種結構中受益,因為它的結構總是相當清晰。

在 Java 中,方法不能脫離類而存在。但是,您可以采取相關步驟,使方法成為單個類的、公共可見的靜态方法。作為示例,您可以采用類似下面這樣的一個類:

class Polygon {

.

public int getPerimeter() {...}

public boolean isConvex() {...}

public boolean containsPoint(Point p) {...}

}

并将其更改為類似以下的形式:

public int getPerimeter() {return pPolygon.computePerimeter(this);}

public boolean isConvex() {return pPolygon.isConvex(this);}

public boolean containsPoint(Point p) {return pPolygon.containsPoint(this, p);}

其中,pPolygon 如下所示:

class pPolygon {

static public int computePerimeter(Polygon polygon) {...}

static public boolean isConvex(Polygon polygon) {...}

static public boolean containsPoint(Polygon polygon, Point p) {...}

類名 pPolygon 反映了該類所封裝的過程主要與類型 Polygon 的對象有關。類名前的 p 表示該類的唯一用途就是将公共可見的靜态過程組織起來。然而,在 Java 中類名以小寫字母開頭是不規範的,像 pPolygon 這樣的類并不完成正常的類功能。這就是說,它不代表一類對象;它隻是該語言所需的一個組織實體。

在以上事例中所作更改的全部效果就是,用戶端代碼不再非要通過繼承 Polygon 來重用其功能。現在這一功能在 pPolygon 類中是以過程為機關提供的。用戶端代碼僅使用它所需的功能,而不必關心它不需要的功能。

這并不意味着類不會在新的過程式程式設計風格中發揮積極作用。恰恰相反,類要執行必要的分組任務,并封裝它們所代表的對象的資料成員。此外,類通過實作多個接口而具備的多态性使其具備了卓越的可重用性,請參閱第二步中的說明。但是,您應該将通過類繼承獲得可重用性和多态性的方法歸類到優先級較低的技術中,因為将功能包含在執行個體方法中并不是實作可重用性的最佳選擇。

四人合著的暢銷書 Design Patterns 簡要提及了一種與這一技術隻有細微差别的技術。那本書中的 Strategy 模式提倡用一個共公接口将相關算法的每個系列成員都封裝起來,以便用戶端代碼可互換這些算法。因為一種算法通常被編寫為一個或幾個獨立的過程,因而這種封裝強調重用執行單一任務(即一個算法)的過程,而不強調重用包含代碼和資料、執行多項任務的對象。本步驟也展現了同樣的基本思想。

然而,用接口封裝算法意味着将算法編寫為實作該接口的一個對象。這意味着我們仍然被束縛在與資料耦合在一起的過程及其封裝對象的其他方法上,因而使重用變得複雜。每次使用算法時必須執行個體化這些對象也是個問題,這将降低程式的性能。幸運的是, Design Patterns 提供的一種解決方案可解決這兩個問題。在編寫 Strategy 對象時您可使用 Flyweight 模式,以使每個對象僅有一個衆所周知的共享執行個體(該執行個體處理執行問題),這樣每個共享對象就不會在兩次通路之間維護狀态(是以該對象不包含任何成員變量,進而解決了許多耦合問題)。生成的 Flyweight-Strategy 模式将本步驟中封裝功能的技術高度內建在全局可用的無狀态過程中。

第二步:将非基本資料類型的輸入參數類型轉換為接口類型

通過接口參數類型而非通過類繼承利用多态性,這是在面向對象程式設計方法中實作可重用性的真正基礎,正如 Allen Holub 在 "Build User Interfaces for Object-Oriented Systems, Part 2" 中所講的那樣。

“... 可重用性是通過編寫接口,而不是通過編寫類來實作的。如果一個方法的所有參數均為一些已知接口的引用,而這些接口又是由您從未聽過的一些類實作的,那麼該方法可對編寫代碼時還不存在的類的對象進行操作。從技術上講,可重用的是方法,而不是傳遞給該方法的對象。”

将 Holub 的論述應用到第一步的結果,一旦某個功能塊可作為一個全局可見的獨立過程,您就可以通過将它的每個類級輸入參數類型轉換為接口類型,進而進一步提高它的可重用性。這樣,實作該接口類型的任何類的對象都符合該參數的要求,而不僅僅是符合原始類的要求。這樣,該過程便潛在地可用于更多的對象類型。

例如,假定您有一個全局可見的靜态方法:

static public boolean contains(Rectangle rect, int x, int y) {...}

該方法旨在判斷給定的矩形是否包含給定的位置。此處您應該将 rect 參數的類型從類類型 Rectangle 更改為接口類型,如下所示:

static public boolean contains(Rectangular rect, int x, int y) {...}

Rectangular could be the following interface:

public interface Rectangular {

Rectangle getBounds();

現在,可描述為 Rectangular 的類(即可實作 Rectangular 接口)的對象都可作為 rect 的參數傳遞給 pRectangular.contains()。我們通過放寬對可傳遞給方法的參數的限制來提高方法的可重用性。

但是,就以上示例而言,當 Rectangle 接口的 getBounds 方法傳回一個 Rectangle 時,您可能不知道使用 Rectangular 接口會有什麼實際的好處;也就是說,如果我們知道我們要傳入的對象在被請求時能傳回 Rectangle;為什麼不傳入 Rectangle 類型而要傳入接口類型呢?最重要的原因與集合有關。假定有這樣一個方法:

static public boolean areAnyOverlapping(Collection rects) {...}

該方法旨在判斷給定集合中的 rectangular 對象是否有重疊。接下來,在方法體中,當您依次處理集合中的每個對象時,如果無法将對象轉換為諸如 Rectangular 這樣的接口類型,如何才能通路那個對象的 rectangle 呢?唯一的選擇是将對象轉換為特定的類類型(我們已知該類中有一個方法能提供 rectangle),這意味着該方法必須事先知道它要對何種類類型進行操作,是以重用它時隻能使用這些類型。這就是這一步首先要避免的問題!

第三步:選擇耦合性較小的輸入參數接口類型

在執行第二步時,應該選擇何種接口類型來替代給定的類類型呢?答案是:能充分描述過程對參數的要求且累贅最少的任何接口。參數對象要實作的接口越小,任一特定類能實作該接口的機會就越大 -- 因而其對象可用作該參數的類的數量也就越多。很容易看出,如果您有如下這樣一個方法:

static public boolean areOverlapping(Window window1, Window window2) {...}

該方法旨在判斷兩個(假定為 rectangular)視窗是否重疊,如果該方法僅要求它的兩個參數提供它們各自的 rectangular 坐标,則最好簡化這兩個參數的類型以反映這一事實:

static public boolean areOverlapping(Rectangular rect1, Rectangular rect2) {...}

以上代碼假定前面的 Window 類型對象也能實作 Rectangular。現在您就可以重用任何 rectangular 對象的第一個方法中所包含的功能。

您可能有過多次這樣的經曆,即充分指定了參數要求的可用接口包含過多不必要的方法。碰到這種情況時,您就應在全局名稱空間中定義一個新的公共接口,以便其他可能面臨同樣窘境的方法重用這個接口。

您也可能有過多次這樣的經曆,即最好建立一個獨特的接口來指定單個過程對一個參數的要求。您所建立的接口隻會用于那個參數。當您希望将參數當作 C 中的函數指針處理時經常會出現這種情況,例如,假定有這樣一個過程:

static public void sort(List list, SortComparison comp) {...}

該過程通過使用給定的比較對象 comp 對清單的所有對象進行比較,進而對給定的清單進行排序,sort 對 comp 的全部要求就是調用其單個方法執行比較。是以,SortComparison 應該是僅包含一個方法的接口:

public interface SortComparison {

boolean comesBefore(Object a, Object b);

該接口的唯一用途就是為 sort 提供一種通路完成其工作所需功能的方法,是以 SortComparison 不應在别處重用。

小結

以上三步旨在改進用更傳統的面向對象方法編寫的現有代碼。将這三個步驟與面向對象程式設計結合使用即可建構一種新的方法,您可用這種新方法編寫以後的代碼,這樣編寫代碼将提高方法的可重用性和内聚性,同時也會減少方法的互相耦合及複雜性。

很明顯,您不應該對本質上不适合重用的代碼執行這些步驟。這種代碼通常存在于程式的表示層。建立程式使用者界面的代碼及将輸入事件綁定到完成實際操作的控制代碼是不可重用的兩個例子,因為它們的功能随程式的不同而相差甚遠,根本無法實作可重用性。