上述設計方案的一個問題是仍然需要一個中心場所,必須在那裡知道所有類型的對象:在factory()方法內部。如果經常都要向系統添加新類型,factory()方法為每種新類型都要修改一遍。若確實對這個問題感到苦惱,可試試再深入一步,將與類型有關的所有信息——包括它的創建過程——都移入代表那種類型的類內部。這樣一來,每次新添一種類型的時候,需要做的唯一事情就是從一個類繼承。
為將涉及類型創建的信息移入特定類型的Trash裡,必須使用“原型”(prototype)范式(來自《Design Patterns》那本書)。這裡最基本的想法是我們有一個主控對象序列,為自己感興趣的每種類型都制作一個。這個序列中的對象只能用於新對象的創建,采用的操作類似內建到Java根類Object內部的clone()機制。在這種情況下,我們將克隆方法命名為tClone()。准備創建一個新對象時,要事先收集好某種形式的信息,用它建立我們希望的對象類型。然後在主控序列中遍歷,將手上的信息與主控序列中原型對象內任何適當的信息作對比。若找到一個符合自己需要的,就克隆它。
采用這種方案,我們不必用硬編碼的方式植入任何創建信息。每個對象都知道如何揭示出適當的信息,以及如何對自身進行克隆。所以一種新類型加入系統的時候,factory()方法不需要任何改變。
為解決原型的創建問題,一個方法是添加大量方法,用它們支持新對象的創建。但在Java 1.1中,如果擁有指向Class對象的一個句柄,那麼它已經提供了對創建新對象的支持。利用Java 1.1的“反射”(已在第11章介紹)技術,即便我們只有指向Class對象的一個句柄,亦可正常地調用一個構建器。這對原型問題的解決無疑是個完美的方案。
原型列表將由指向所有想創建的Class對象的一個句柄列表間接地表示。除此之外,假如原型處理失敗,則factory()方法會認為由於一個特定的Class對象不在列表中,所以會嘗試裝載它。通過以這種方式動態裝載原型,Trash類根本不需要知道自己要操縱的是什麼類型。因此,在我們添加新類型時不需要作出任何形式的修改。於是,我們可在本章剩余的部分方便地重復利用它。
//: Trash.java // Base class for Trash recycling examples package c16.trash; import java.util.*; import java.lang.reflect.*; public abstract class Trash { private double weight; Trash(double wt) { weight = wt; } Trash() {} public abstract double value(); public double weight() { return weight; } // Sums the value of Trash in a bin: public static void sumValue(Vector bin) { Enumeration e = bin.elements(); double val = 0.0f; while(e.hasMoreElements()) { // One kind of RTTI: // A dynamically-checked cast Trash t = (Trash)e.nextElement(); val += t.weight() * t.value(); System.out.println( "weight of " + // Using RTTI to get type // information about the class: t.getClass().getName() + " = " + t.weight()); } System.out.println("Total value = " + val); } // Remainder of class provides support for // prototyping: public static class PrototypeNotFoundException extends Exception {} public static class CannotCreateTrashException extends Exception {} private static Vector trashTypes = new Vector(); public static Trash factory(Info info) throws PrototypeNotFoundException, CannotCreateTrashException { for(int i = 0; i < trashTypes.size(); i++) { // Somehow determine the new type // to create, and create one: Class tc = (Class)trashTypes.elementAt(i); if (tc.getName().indexOf(info.id) != -1) { try { // Get the dynamic constructor method // that takes a double argument: Constructor ctor = tc.getConstructor( new Class[] {double.class}); // Call the constructor to create a // new object: return (Trash)ctor.newInstance( new Object[]{new Double(info.data)}); } catch(Exception ex) { ex.printStackTrace(); throw new CannotCreateTrashException(); } } } // Class was not in the list. Try to load it, // but it must be in your class path! try { System.out.println("Loading " + info.id); trashTypes.addElement( Class.forName(info.id)); } catch(Exception e) { e.printStackTrace(); throw new PrototypeNotFoundException(); } // Loaded successfully. Recursive call // should work this time: return factory(info); } public static class Info { public String id; public double data; public Info(String name, double data) { id = name; this.data = data; } } } ///:~
基本Trash類和sumValue()還是象往常一樣。這個類剩下的部分支持原型范式。大家首先會看到兩個內部類(被設為static屬性,使其成為只為代碼組織目的而存在的內部類),它們描述了可能出現的違例。在它後面跟隨的是一個Vector trashTypes,用於容納Class句柄。
在Trash.factory()中,Info對象id(Info類的另一個版本,與前面討論的不同)內部的String包含了要創建的那種Trash的類型名稱。這個String會與列表中的Class名比較。若存在相符的,那便是要創建的對象。當然,還有很多方法可以決定我們想創建的對象。之所以要采用這種方法,是因為從一個文件讀入的信息可以轉換成對象。
發現自己要創建的Trash(垃圾)種類後,接下來就輪到“反射”方法大顯身手了。getConstructor()方法需要取得自己的參數——由Class句柄構成的一個數組。這個數組代表著不同的參數,並按它們正確的順序排列,以便我們查找的構建器使用。在這兒,該數組是用Java 1.1的數組創建語法動態創建的:
new Class[] {double.class}
這個代碼假定所有Trash類型都有一個需要double數值的構建器(注意double.class與Double.class是不同的)。若考慮一種更靈活的方案,亦可調用getConstructors(),令其返回可用構建器的一個數組。
從getConstructors()返回的是指向一個Constructor對象的句柄(該對象是java.lang.reflect的一部分)。我們用方法newInstance()動態地調用構建器。該方法需要獲取包含了實際參數的一個Object數組。這個數組同樣是按Java 1.1的語法創建的:
new Object[] {new Double(info.data)}
在這種情況下,double必須置入一個封裝(容器)類的內部,使其真正成為這個對象數組的一部分。通過調用newInstance(),會提取出double,但大家可能會覺得稍微有些迷惑——參數既可能是double,也可能是Double,但在調用的時候必須用Double傳遞。幸運的是,這個問題只存在於基本數據類型中間。
理解了具體的過程後,再來創建一個新對象,並且只為它提供一個Class句柄,事情就變得非常簡單了。就目前的情況來說,內部循環中的return永遠不會執行,我們在終點就會退出。在這兒,程序動態裝載Class對象,並把它加入trashTypes(垃圾類型)列表,從而試圖糾正這個問題。若仍然找不到真正有問題的地方,同時裝載又是成功的,那麼就重復調用factory方法,重新試一遍。
正如大家會看到的那樣,這種設計方案最大的優點就是不需要改動代碼。無論在什麼情況下,它都能正常地使用(假定所有Trash子類都包含了一個構建器,用以獲取單個double參數)。
1. Trash子類
為了與原型機制相適應,對Trash每個新子類唯一的要求就是在其中包含了一個構建器,指示它獲取一個double參數。Java 1.1的“反射”機制可負責剩下的所有工作。
下面是不同類型的Trash,每種類型都有它們自己的文件裡,但都屬於Trash包的一部分(同樣地,為了方便在本章內重復使用):
//: Aluminum.java // The Aluminum class with prototyping package c16.trash; public class Aluminum extends Trash { private static double val = 1.67f; public Aluminum(double wt) { super(wt); } public double value() { return val; } public static void value(double newVal) { val = newVal; } } ///:~
下面是一種新的Trash類型:
//: Cardboard.java // The Cardboard class with prototyping package c16.trash; public class Cardboard extends Trash { private static double val = 0.23f; public Cardboard(double wt) { super(wt); } public double value() { return val; } public static void value(double newVal) { val = newVal; } } ///:~
可以看出,除構建器以外,這些類根本沒有什麼特別的地方。
2. 從外部文件中解析出Trash
與Trash對象有關的信息將從一個外部文件中讀取。針對Trash的每個方面,文件內列出了所有必要的信息——每行都代表一個方面,采用“垃圾(廢品)名稱:值”的固定格式。例如:
c16.Trash.Glass:54 c16.Trash.Paper:22 c16.Trash.Paper:11 c16.Trash.Glass:17 c16.Trash.Aluminum:89 c16.Trash.Paper:88 c16.Trash.Aluminum:76 c16.Trash.Cardboard:96 c16.Trash.Aluminum:25 c16.Trash.Aluminum:34 c16.Trash.Glass:11 c16.Trash.Glass:68 c16.Trash.Glass:43 c16.Trash.Aluminum:27 c16.Trash.Cardboard:44 c16.Trash.Aluminum:18 c16.Trash.Paper:91 c16.Trash.Glass:63 c16.Trash.Glass:50 c16.Trash.Glass:80 c16.Trash.Aluminum:81 c16.Trash.Cardboard:12 c16.Trash.Glass:12 c16.Trash.Glass:54 c16.Trash.Aluminum:36 c16.Trash.Aluminum:93 c16.Trash.Glass:93 c16.Trash.Paper:80 c16.Trash.Glass:36 c16.Trash.Glass:12 c16.Trash.Glass:60 c16.Trash.Paper:66 c16.Trash.Aluminum:36 c16.Trash.Cardboard:22
注意在給定類名的時候,類路徑必須包含在內,否則就找不到類。
為解析它,每一行內容都會讀入,並用字串方法indexOf()來建立“:”的一個索引。首先用字串方法substring()取出垃圾的類型名稱,接著用一個靜態方法Double.valueOf()取得相應的值,並轉換成一個double值。trim()方法則用於刪除字串兩頭的多余空格。
Trash解析器置入單獨的文件中,因為本章將不斷地用到它。如下所示:
//: ParseTrash.java // Open a file and parse its contents into // Trash objects, placing each into a Vector package c16.trash; import java.util.*; import java.io.*; public class ParseTrash { public static void fillBin(String filename, Fillable bin) { try { BufferedReader data = new BufferedReader( new FileReader(filename)); String buf; while((buf = data.readLine())!= null) { String type = buf.substring(0, buf.indexOf(':')).trim(); double weight = Double.valueOf( buf.substring(buf.indexOf(':') + 1) .trim()).doubleValue(); bin.addTrash( Trash.factory( new Trash.Info(type, weight))); } data.close(); } catch(IOException e) { e.printStackTrace(); } catch(Exception e) { e.printStackTrace(); } } // Special case to handle Vector: public static void fillBin(String filename, Vector bin) { fillBin(filename, new FillableVector(bin)); } } ///:~
在RecycleA.java中,我們用一個Vector容納Trash對象。然而,亦可考慮采用其他集合類型。為做到這一點,fillBin()的第一個版本將獲取指向一個Fillable的句柄。後者是一個接口,用於支持一個名為addTrash()的方法:
//: Fillable.java // Any object that can be filled with Trash package c16.trash; public interface Fillable { void addTrash(Trash t); } ///:~
支持該接口的所有東西都能伴隨fillBin使用。當然,Vector並未實現Fillable,所以它不能工作。由於Vector將在大多數例子中應用,所以最好的做法是添加另一個過載的fillBin()方法,令其以一個Vector作為參數。利用一個適配器(Adapter)類,這個Vector可作為一個Fillable對象使用:
//: FillableVector.java // Adapter that makes a Vector Fillable package c16.trash; import java.util.*; public class FillableVector implements Fillable { private Vector v; public FillableVector(Vector vv) { v = vv; } public void addTrash(Trash t) { v.addElement(t); } } ///:~
可以看到,這個類唯一的任務就是負責將Fillable的addTrash()同Vector的addElement()方法連接起來。利用這個類,已過載的fillBin()方法可在ParseTrash.java中伴隨一個Vector使用:
public static void fillBin(String filename, Vector bin) { fillBin(filename, new FillableVector(bin)); }
這種方案適用於任何頻繁用到的集合類。除此以外,集合類還可提供它自己的適配器類,並實現Fillable(稍後即可看到,在DynaTrash.java中)。
3. 原型機制的重復應用
現在,大家可以看到采用原型技術的、修訂過的RecycleA.java版本了:
//: RecycleAP.java // Recycling with RTTI and Prototypes package c16.recycleap; import c16.trash.*; import java.util.*; public class RecycleAP { public static void main(String[] args) { Vector bin = new Vector(); // Fill up the Trash bin: ParseTrash.fillBin("Trash.dat", bin); Vector glassBin = new Vector(), paperBin = new Vector(), alBin = new Vector(); Enumeration sorter = bin.elements(); // Sort the Trash: while(sorter.hasMoreElements()) { Object t = sorter.nextElement(); // RTTI to show class membership: if(t instanceof Aluminum) alBin.addElement(t); if(t instanceof Paper) paperBin.addElement(t); if(t instanceof Glass) glassBin.addElement(t); } Trash.sumValue(alBin); Trash.sumValue(paperBin); Trash.sumValue(glassBin); Trash.sumValue(bin); } } ///:~
所有Trash對象——以及ParseTrash及支撐類——現在都成為名為c16.trash的一個包的一部分,所以它們可以簡單地導入。
無論打開包含了Trash描述信息的數據文件,還是對那個文件進行解析,所有涉及到的操作均已封裝到static(靜態)方法ParseTrash.fillBin()裡。所以它現在已經不是我們設計過程中要注意的一個重點。在本章剩余的部分,大家經常都會看到無論添加的是什麼類型的新類,ParseTrash.fillBin()都會持續工作,不會發生改變,這無疑是一種優良的設計方案。
提到對象的創建,這一方案確實已將新類型加入系統所需的變動嚴格地“本地化”了。但在使用RTTI的過程中,卻存在著一個嚴重的問題,這裡已明確地顯露出來。程序表面上工作得很好,但卻永遠偵測到不能“硬紙板”(Cardboard)這種新的廢品類型——即使列表裡確實有一個硬紙板類型!之所以會出現這種情況,完全是由於使用了RTTI的緣故。RTTI只會查找那些我們告訴它查找的東西。RTTI在這裡錯誤的用法是“系統中的每種類型”都進行了測試,而不是僅測試一種類型或者一個類型子集。正如大家以後會看到的那樣,在測試每一種類型時可換用其他方式來運用多形性特征。但假如以這種形式過多地使用RTTI,而且又在自己的系統裡添加了一種新類型,很容易就會忘記在程序裡作出適當的改動,從而埋下以後難以發現的Bug。因此,在這種情況下避免使用RTTI是很有必要的,這並不僅僅是為了表面好看——也是為了產生更易維護的代碼。