?? 0122.htm
字號:
930頁程序<br>
<br>
可以看到,這個類唯一的任務就是負責將Fillable的addTrash()同Vector的addElement()方法連接起來。利用這個類,已過載的fillBin()方法可在ParseTrash.java中伴隨一個Vector使用:<br>
<br>
930頁下程序<br>
<br>
這種方案適用于任何頻繁用到的集合類。除此以外,集合類還可提供它自己的適配器類,并實現Fillable(稍后即可看到,在DynaTrash.java中)。<br>
<br>
3. 原型機制的重復應用<br>
現在,大家可以看到采用原型技術的、修訂過的RecycleA.java版本了:<br>
<br>
931頁程序<br>
<br>
所有Trash對象——以及ParseTrash及支撐類——現在都成為名為c16.trash的一個包的一部分,所以它們可以簡單地導入。<br>
無論打開包含了Trash描述信息的數據文件,還是對那個文件進行解析,所有涉及到的操作均已封裝到static(靜態)方法ParseTrash.fillBin()里。所以它現在已經不是我們設計過程中要注意的一個重點。在本章剩余的部分,大家經常都會看到無論添加的是什么類型的新類,ParseTrash.fillBin()都會持續工作,不會發生改變,這無疑是一種優良的設計方案。<br>
提到對象的創建,這一方案確實已將新類型加入系統所需的變動嚴格地“本地化”了。但在使用RTTI的過程中,卻存在著一個嚴重的問題,這里已明確地顯露出來。程序表面上工作得很好,但卻永遠偵測到不能“硬紙板”(Cardboard)這種新的廢品類型——即使列表里確實有一個硬紙板類型!之所以會出現這種情況,完全是由于使用了RTTI的緣故。RTTI只會查找那些我們告訴它查找的東西。RTTI在這里錯誤的用法是“系統中的每種類型”都進行了測試,而不是僅測試一種類型或者一個類型子集。正如大家以后會看到的那樣,在測試每一種類型時可換用其他方式來運用多形性特征。但假如以這種形式過多地使用RTTI,而且又在自己的系統里添加了一種新類型,很容易就會忘記在程序里作出適當的改動,從而埋下以后難以發現的Bug。因此,在這種情況下避免使用RTTI是很有必要的,這并不僅僅是為了表面好看——也是為了產生更易維護的代碼。<br>
<br>
16.5 抽象的應用<br>
走到這一步,接下來該考慮一下設計方案剩下的部分了——在哪里使用類?既然歸類到垃圾箱的辦法非常不雅且過于暴露,為什么不隔離那個過程,把它隱藏到一個類里呢?這就是著名的“如果必須做不雅的事情,至少應將其本地化到一個類里”規則。看起來就象下面這樣:<br>
<br>
932頁圖<br>
<br>
現在,只要一種新類型的Trash加入方法,對TrashSorter對象的初始化就必須變動。可以想象,TrashSorter類看起來應該象下面這個樣子:<br>
class TrashSorter extends Vector {<br>
void sort(Trash t) { /* ... */ }<br>
}<br>
也就是說,TrashSorter是由一系列句柄構成的Vector(系列),而那些句柄指向的又是由Trash句柄構成的Vector;利用addElement(),可以安裝新的TrashSorter,如下所示:<br>
TrashSorter ts = new TrashSorter();<br>
ts.addElement(new Vector());<br>
但是現在,sort()卻成為一個問題。用靜態方式編碼的方法如何應付一種新類型加入的事實呢?為解決這個問題,必須從sort()里將類型信息刪除,使其需要做的所有事情就是調用一個通用方法,用它照料涉及類型處理的所有細節。這當然是對一個動態綁定方法進行描述的另一種方式。所以sort()會在序列中簡單地遍歷,并為每個Vector都調用一個動態綁定方法。由于這個方法的任務是收集它感興趣的垃圾片,所以稱之為grab(Trash)。結構現在變成了下面這樣:<br>
<br>
933頁圖<br>
<br>
其中,TrashSorter需要調用每個grab()方法;然后根據當前Vector容納的是什么類型,會獲得一個不同的結果。也就是說,Vector必須留意自己容納的類型。解決這個問題的傳統方法是創建一個基礎“Trash
bin”(垃圾筒)類,并為希望容納的每個不同的類型都繼承一個新的衍生類。若Java有一個參數化的類型機制,那就也許是最直接的方法。但對于這種機制應該為我們構建的各個類,我們不應該進行麻煩的手工編碼,以后的“觀察”方式提供了一種更好的編碼方式。<br>
OOP設計一條基本的準則是“為狀態的變化使用數據成員,為行為的變化使用多性形”。對于容納Paper(紙張)的Vector,以及容納Glass(玻璃)的Vector,大家最開始或許會認為分別用于它們的grab()方法肯定會產生不同的行為。但具體如何卻完全取決于類型,而不是其他什么東西。可將其解釋成一種不同的狀態,而且由于Java有一個類可表示類型(Class),所以可用它判斷特定的Tbin要容納什么類型的Trash。<br>
用于Tbin的構建器要求我們為其傳遞自己選擇的一個Class。這樣做可告訴Vector它希望容納的是什么類型。隨后,grab()方法用Class
BinType和RTTI來檢查我們傳遞給它的Trash對象是否與它希望收集的類型相符。<br>
下面列出完整的解決方案。設定為注釋的編號(如*1*)便于大家對照程序后面列出的說明。<br>
<br>
934-935頁程序<br>
<br>
(1) TbinList容納一系列Tbin句柄,所以在查找與我們傳遞給它的Trash對象相符的情況時,sort()能通過Tbin繼承。<br>
(2) sortBin()允許我們將一個完整的Tbin傳遞進去,而且它會在Tbin里遍歷,挑選出每種Trash,并將其歸類到特定的Tbin中。請注意這些代碼的通用性:新類型加入時,它本身不需要任何改動。只要新類型加入(或發生其他事件)時大量代碼都不需要變化,就表明我們設計的是一個容易擴展的系統。<br>
(3)
現在可以體會添加新類型有多么容易了。為支持添加,只需要改動幾行代碼。如確實有必要,甚至可以進一步地改進設計,使更多的代碼都保持“固定”。<br>
(4) 一個方法調用使bin的內容歸類到對應的、特定類型的垃圾筒里。<br>
<br>
16.6 多重派遣<br>
上述設計方案肯定是令人滿意的。系統內新類型的加入涉及添加或修改不同的類,但沒有必要在系統內對代碼作大范圍的改動。除此以外,RTTI并不象它在RecycleA.java里那樣被不當地使用。然而,我們仍然有可能更深入一步,以最“純”的角度來看待RTTI,考慮如何在垃圾分類系統中將它完全消滅。<br>
為達到這個目標,首先必須認識到:對所有與不同類型有特殊關聯的活動來說——比如偵測一種垃圾的具體類型,并把它置入適當的垃圾筒里——這些活動都應當通過多形性以及動態綁定加以控制。<br>
以前的例子都是先按類型排序,再對屬于某種特殊類型的一系列元素進行操作。現在一旦需要操作特定的類型,就請先停下來想一想。事實上,多形性(動態綁定的方法調用)整個的宗旨就是幫我們管理與不同類型有特殊關聯的信息。既然如此,為什么還要自己去檢查類型呢?<br>
答案在于大家或許不以為然的一個道理:Java只執行單一派遣。也就是說,假如對多個類型未知的對象執行某項操作,Java只會為那些類型中的一種調用動態綁定機制。這當然不能解決問題,所以最后不得不人工判斷某些類型,才能有效地產生自己的動態綁定行為。<br>
為解決這個缺陷,我們需要用到“多重派遣”機制,這意味著需要建立一個配置,使單一方法調用能產生多個動態方法調用,從而在一次處理過程中正確判斷出多種類型。為達到這個要求,需要對多個類型結構進行操作:每一次派遣都需要一個類型結構。下面的例子將對兩個結構進行操作:現有的Trash系列以及由垃圾筒(Trash
Bin)的類型構成的一個系列——不同的垃圾或廢品將置入這些筒內。第二個分級結構并非絕對顯然的。在這種情況下,我們需要人為地創建它,以執行多重派遣(由于本例只涉及兩次派遣,所以稱為“雙重派遣”)。<br>
<br>
16.6.1 實現雙重派遣<br>
記住多形性只能通過方法調用才能表現出來,所以假如想使雙重派遣正確進行,必須執行兩個方法調用:在每種結構中都用一個來判斷其中的類型。在Trash結構中,將使用一個新的方法調用addToBin(),它采用的參數是由TypeBin構成的一個數組。那個方法將在數組中遍歷,嘗試將自己加入適當的垃圾筒,這里正是雙重派遣發生的地方。<br>
<br>
937頁圖<br>
<br>
新建立的分級結構是TypeBin,其中包含了它自己的一個方法,名為add(),而且也應用了多形性。但要注意一個新特點:add()已進行了“過載”處理,可接受不同的垃圾類型作為參數。因此,雙重滿足機制的一個關鍵點是它也要涉及到過載。<br>
程序的重新設計也帶來了一個問題:現在的基礎類Trash必須包含一個addToBin()方法。為解決這個問題,一個最直接的辦法是復制所有代碼,并修改基礎類。然而,假如沒有對源碼的控制權,那么還有另一個辦法可以考慮:將addToBin()方法置入一個接口內部,保持Trash不變,并繼承新的、特殊的類型Aluminum,Paper,Glass以及Cardboard。我們在這里準備采取后一個辦法。<br>
這個設計方案中用到的大多數類都必須設為public(公用)屬性,所以它們放置于自己的類內。下面列出接口代碼:<br>
<br>
938頁上程序<br>
<br>
在Aluminum,Paper,Glass以及Cardboard每個特定的子類型內,都會實現接口TypeBinMember的addToBin()方法,但每種情況下使用的代碼“似乎”都是完全一樣的:<br>
<br>
938-940頁程序<br>
<br>
每個addToBin()內的代碼會為數組中的每個TypeBin對象調用add()。但請注意參數:this。對Trash的每個子類來說,this的類型都是不同的,所以不能認為代碼“完全”一樣——盡管以后在Java里加入參數化類型機制后便可認為一樣。這是雙重派遣的第一個部分,因為一旦進入這個方法內部,便可知道到底是Aluminum,Paper,還是其他什么垃圾類型。在對add()的調用過程中,這種信息是通過this的類型傳遞的。編譯器會分析出對add()正確的過載版本的調用。但由于tb[i]會產生指向基礎類型TypeBin的一個句柄,所以最終會調用一個不同的方法——具體什么方法取決于當前選擇的TypeBin的類型。那就是第二次派遣。<br>
下面是TypeBin的基礎類:<br>
<br>
940頁程序<br>
<br>
可以看到,過載的add()方法全都會返回false。如果未在衍生類里對方法進行過載,它就會一直返回false,而且調用者(目前是addToBin())會認為當前Trash對象尚未成功加入一個集合,所以會繼續查找正確的集合。<br>
在TypeBin的每一個子類中,都只有一個過載的方法會被過載——具體取決于準備創建的是什么垃圾筒類型。舉個例子來說,CardboardBin會過載add(DDCardboard)。過載的方法會將垃圾對象加入它的集合,并返回true。而CardboardBin中剩余的所有add()方法都會繼續返回false,因為它們尚未過載。事實上,假如在這里采用了參數化類型機制,Java代碼的自動創建就要方便得多(使用C++的“模板”,我們不必費事地為子類編碼,或者將addToBin()方法置入Trash里;Java在這方面尚有待改進)。<br>
由于對這個例子來說,垃圾的類型已經定制并置入一個不同的目錄,所以需要用一個不同的垃圾數據文件令其運轉起來。下面是一個示范性的DDTrash.dat:<br>
<br>
941-942頁程序<br>
<br>
下面列出程序剩余的部分:<br>
<br>
942-943頁程序<br>
<br>
其中,TrashBinSet封裝了各種不同類型的TypeBin,同時還有sortIntoBins()方法。所有雙重派遣事件都會在那個方法里發生。可以看到,一旦設置好結構,再歸類成各種TypeBin的工作就變得十分簡單了。除此以外,兩個動態方法調用的效率可能也比其他排序方法高一些。<br>
注意這個系統的方便性主要體現在main()中,同時還要注意到任何特定的類型信息在main()中都是完全獨立的。只與Trash基礎類接口通信的其他所有方法都不會受到Trash類中發生的改變的干擾。<br>
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -