JAVA范式--“制作更多的對象”
發(fā)布時間:2008-08-06 閱讀數(shù): 次 來源:網(wǎng)樂原科技
首先考慮Trash對象首次創(chuàng)建的地方,這是main()里的一個switch語句:
for(int i = 0; i < 30; i++)
switch((int)(Math.random() * 3)) {
case 0 :
bin.addElement(new
Aluminum(Math.random() * 100));
break;
case 1 :
bin.addElement(new
Paper(Math.random() * 100));
break;
case 2 :
bin.addElement(new
Glass(Math.random() * 100));
}
這些代碼顯然“過于復雜”,也是新類型加入時必須改動代碼的場所之一。如果經(jīng)常都要加入新類型,那么更好的方案就是建立一個獨立的方法,用它獲取所有必需的信息,并創(chuàng)建一個句柄,指向正確類型的一個對象——已經(jīng)上溯造型到一個Trash對象。在《Design Patterns》中,它被粗略地稱呼為“創(chuàng)建范式”。要在這里應用的特殊范式是Factory方法的一種變體。在這里,F(xiàn)actory方法屬于Trash的一名static(靜態(tài))成員。但更常見的一種情況是:它屬于衍生類中一個被過載的方法。
Factory方法的基本原理是我們將創(chuàng)建對象所需的基本信息傳遞給它,然后返回并等候句柄(已經(jīng)上溯造型至基礎類型)作為返回值出現(xiàn)。從這時開始,就可以按多形性的方式對待對象了。因此,我們根本沒必要知道所創(chuàng)建對象的準確類型是什么。事實上,F(xiàn)actory方法會把自己隱藏起來,我們是看不見它的。這樣做可防止不慎的誤用。如果想在沒有多形性的前提下使用對象,必須明確地使用RTTI和指定造型。
但仍然存在一個小問題,特別是在基礎類中使用更復雜的方法(不是在這里展示的那種),且在衍生類里過載(覆蓋)了它的前提下。如果在衍生類里請求的信息要求更多或者不同的參數(shù),那么該怎么辦呢?“創(chuàng)建更多的對象”解決了這個問題。為實現(xiàn)Factory方法,Trash類使用了一個新的方法,名為factory。為了將創(chuàng)建數(shù)據(jù)隱藏起來,我們用一個名為Info的新類包含factory方法創(chuàng)建適當?shù)腡rash對象時需要的全部信息。下面是Info一種簡單的實現(xiàn)方式:
class Info {
int type;
// Must change this to add another type:
static final int MAX_NUM = 4;
double data;
Info(int typeNum, double dat) {
type = typeNum % MAX_NUM;
data = dat;
}
}
Info對象唯一的任務就是容納用于factory()方法的信息?,F(xiàn)在,假如出現(xiàn)了一種特殊情況,factory()需要更多或者不同的信息來新建一種類型的Trash對象,那么再也不需要改動factory()了。通過添加新的數(shù)據(jù)和構建器,我們可以修改Info類,或者采用子類處理更典型的面向對象形式。
用于這個簡單示例的factory()方法如下:
static Trash factory(Info i) {
switch(i.type) {
default: // To quiet the compiler
case 0:
return new Aluminum(i.data);
case 1:
return new Paper(i.data);
case 2:
return new Glass(i.data);
// Two lines here:
case 3:
return new Cardboard(i.data);
}
}
在這里,對象的準確類型很容易即可判斷出來。但我們可以設想一些更復雜的情況,factory()將采用一種復雜的算法。無論如何,現(xiàn)在的關鍵是它已隱藏到某個地方,而且我們在添加新類型時知道去那個地方。
新對象在main()中的創(chuàng)建現(xiàn)在變得非常簡單和清爽:
for(int i = 0; i < 30; i++)
bin.addElement(
Trash.factory(
new Info(
(int)(Math.random() * Info.MAX_NUM),
Math.random() * 100)));
我們在這里創(chuàng)建了一個Info對象,用于將數(shù)據(jù)傳入factory();后者在內(nèi)存堆中創(chuàng)建某種Trash對象,并返回添加到Vector bin內(nèi)的句柄。當然,如果改變了參數(shù)的數(shù)量及類型,仍然需要修改這個語句。但假如Info對象的創(chuàng)建是自動進行的,也可以避免那個麻煩。例如,可將參數(shù)的一個Vector傳遞到Info對象的構建器中(或直接傳入一個factory()調(diào)用)。這要求在運行期間對參數(shù)(自變量)進行分析與檢查,但確實提供了非常高的靈活程度。
大家從這個代碼可看出Factory要負責解決的“領頭變化”問題:如果向系統(tǒng)添加了新類型(發(fā)生了變化),唯一需要修改的代碼在Factory內(nèi)部,所以Factory將那種變化的影響隔離出來了。
16.4.2 用于原型創(chuàng)建的一個范式
上述設計方案的一個問題是仍然需要一個中心場所,必須在那里知道所有類型的對象:在factory()方法內(nèi)部。如果經(jīng)常都要向系統(tǒng)添加新類型,factory()方法為每種新類型都要修改一遍。若確實對這個問題感到苦惱,可試試再深入一步,將與類型有關的所有信息——包括它的創(chuàng)建過程——都移入代表那種類型的類內(nèi)部。這樣一來,每次新添一種類型的時候,需要做的唯一事情就是從一個類繼承。
為將涉及類型創(chuàng)建的信息移入特定類型的Trash里,必須使用“原型”(prototype)范式(來自《Design Patterns》那本書)。這里最基本的想法是我們有一個主控對象序列,為自己感興趣的每種類型都制作一個。這個序列中的對象只能用于新對象的創(chuàng)建,采用的操作類似內(nèi)建到Java根類Object內(nèi)部的clone()機制。在這種情況下,我們將克隆方法命名為tClone()。準備創(chuàng)建一個新對象時,要事先收集好某種形式的信息,用它建立我們希望的對象類型。然后在主控序列中遍歷,將手上的信息與主控序列中原型對象內(nèi)任何適當?shù)男畔⒆鲗Ρ?。若找到一個符合自己需要的,就克隆它。
采用這種方案,我們不必用硬編碼的方式植入任何創(chuàng)建信息。每個對象都知道如何揭示出適當?shù)男畔?,以及如何對自身進行克隆。所以一種新類型加入系統(tǒng)的時候,factory()方法不需要任何改變。
為解決原型的創(chuàng)建問題,一個方法是添加大量方法,用它們支持新對象的創(chuàng)建。但在Java 1.1中,如果擁有指向Class對象的一個句柄,那么它已經(jīng)提供了對創(chuàng)建新對象的支持。利用Java 1.1的“反射”(已在第11章介紹)技術,即便我們只有指向Class對象的一個句柄,亦可正常地調(diào)用一個構建器。這對原型問題的解決無疑是個完美的方案。
原型列表將由指向所有想創(chuàng)建的Class對象的一個句柄列表間接地表示。除此之外,假如原型處理失敗,則factory()方法會認為由于一個特定的Class對象不在列表中,所以會嘗試裝載它。通過以這種方式動態(tài)裝載原型,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()還是象往常一樣。這個類剩下的部分支持原型范式。大家首先會看到兩個內(nèi)部類(被設為static屬性,使其成為只為代碼組織目的而存在的內(nèi)部類),它們描述了可能出現(xiàn)的違例。在它后面跟隨的是一個Vector trashTypes,用于容納Class句柄。
在Trash.factory()中,Info對象id(Info類的另一個版本,與前面討論的不同)內(nèi)部的String包含了要創(chuàng)建的那種Trash的類型名稱。這個String會與列表中的Class名比較。若存在相符的,那便是要創(chuàng)建的對象。當然,還有很多方法可以決定我們想創(chuàng)建的對象。之所以要采用這種方法,是因為從一個文件讀入的信息可以轉換成對象。
發(fā)現(xiàn)自己要創(chuàng)建的Trash(垃圾)種類后,接下來就輪到“反射”方法大顯身手了。getConstructor()方法需要取得自己的參數(shù)——由Class句柄構成的一個數(shù)組。這個數(shù)組代表著不同的參數(shù),并按它們正確的順序排列,以便我們查找的構建器使用。在這兒,該數(shù)組是用Java 1.1的數(shù)組創(chuàng)建語法動態(tài)創(chuàng)建的:
new Class[] {double.class}
這個代碼假定所有Trash類型都有一個需要double數(shù)值的構建器(注意double.class與Double.class是不同的)。若考慮一種更靈活的方案,亦可調(diào)用getConstructors(),令其返回可用構建器的一個數(shù)組。
從getConstructors()返回的是指向一個Constructor對象的句柄(該對象是java.lang.reflect的一部分)。我們用方法newInstance()動態(tài)地調(diào)用構建器。該方法需要獲取包含了實際參數(shù)的一個Object數(shù)組。這個數(shù)組同樣是按Java 1.1的語法創(chuàng)建的:
new Object[] {new Double(info.data)}
在這種情況下,double必須置入一個封裝(容器)類的內(nèi)部,使其真正成為這個對象數(shù)組的一部分。通過調(diào)用newInstance(),會提取出double,但大家可能會覺得稍微有些迷惑——參數(shù)既可能是double,也可能是Double,但在調(diào)用的時候必須用Double傳遞。幸運的是,這個問題只存在于基本數(shù)據(jù)類型中間。
理解了具體的過程后,再來創(chuàng)建一個新對象,并且只為它提供一個Class句柄,事情就變得非常簡單了。就目前的情況來說,內(nèi)部循環(huán)中的return永遠不會執(zhí)行,我們在終點就會退出。在這兒,程序動態(tài)裝載Class對象,并把它加入trashTypes(垃圾類型)列表,從而試圖糾正這個問題。若仍然找不到真正有問題的地方,同時裝載又是成功的,那么就重復調(diào)用factory方法,重新試一遍。
正如大家會看到的那樣,這種設計方案最大的優(yōu)點就是不需要改動代碼。無論在什么情況下,它都能正常地使用(假定所有Trash子類都包含了一個構建器,用以獲取單個double參數(shù))。
1. Trash子類
為了與原型機制相適應,對Trash每個新子類唯一的要求就是在其中包含了一個構建器,指示它獲取一個double參數(shù)。Java 1.1的“反射”機制可負責剩下的所有工作。
下面是不同類型的Trash,每種類型都有它們自己的文件里,但都屬于Trash包的一部分(同樣地,為了方便在本章內(nèi)重復使用):
//: 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的每個方面,文件內(nèi)列出了所有必要的信息——每行都代表一個方面,采用“垃圾(廢品)名稱:值”的固定格式。例如:
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
注意在給定類名的時候,類路徑必須包含在內(nèi),否則就找不到類。
為解析它,每一行內(nèi)容都會讀入,并用字串方法indexOf()來建立“:”的一個索引。首先用字串方法substring()取出垃圾的類型名稱,接著用一個靜態(tài)方法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并未實現(xiàn)Fillable,所以它不能工作。由于Vector將在大多數(shù)例子中應用,所以最好的做法是添加另一個過載的fillBin()方法,令其以一個Vector作為參數(shù)。利用一個適配器(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));
}
這種方案適用于任何頻繁用到的集合類。除此以外,集合類還可提供它自己的適配器類,并實現(xiàn)Fillable(稍后即可看到,在DynaTrash.java中)。
3. 原型機制的重復應用
現(xiàn)在,大家可以看到采用原型技術的、修訂過的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及支撐類——現(xiàn)在都成為名為c16.trash的一個包的一部分,所以它們可以簡單地導入。
無論打開包含了Trash描述信息的數(shù)據(jù)文件,還是對那個文件進行解析,所有涉及到的操作均已封裝到static(靜態(tài))方法ParseTrash.fillBin()里。所以它現(xiàn)在已經(jīng)不是我們設計過程中要注意的一個重點。在本章剩余的部分,大家經(jīng)常都會看到無論添加的是什么類型的新類,ParseTrash.fillBin()都會持續(xù)工作,不會發(fā)生改變,這無疑是一種優(yōu)良的設計方案。
提到對象的創(chuàng)建,這一方案確實已將新類型加入系統(tǒng)所需的變動嚴格地“本地化”了。但在使用RTTI的過程中,卻存在著一個嚴重的問題,這里已明確地顯露出來。程序表面上工作得很好,但卻永遠偵測到不能“硬紙板”(Cardboard)這種新的廢品類型——即使列表里確實有一個硬紙板類型!之所以會出現(xiàn)這種情況,完全是由于使用了RTTI的緣故。RTTI只會查找那些我們告訴它查找的東西。RTTI在這里錯誤的用法是“系統(tǒng)中的每種類型”都進行了測試,而不是僅測試一種類型或者一個類型子集。正如大家以后會看到的那樣,在測試每一種類型時可換用其他方式來運用多形性特征。但假如以這種形式過多地使用RTTI,而且又在自己的系統(tǒng)里添加了一種新類型,很容易就會忘記在程序里作出適當?shù)母膭樱瑥亩裣乱院箅y以發(fā)現(xiàn)的Bug。因此,在這種情況下避免使用RTTI是很有必要的,這并不僅僅是為了表面好看——也是為了產(chǎn)生更易維護的代碼。