用于原型建设的一个范式
上述设计方案的一个问题是仍然需要一其中心场合,必需在哪里知道所有范例的工具:在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;
}
}
} ///:~
#p#分页标题#e#
根基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
#p#分页标题#e#
留意在给定类名的时候,类路径必需包括在内,不然就找不到类。
为理会它,每一行内容城市读入,并用字串要领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);
}
} ///:~
#p#分页标题#e#
所有Trash工具——以及ParseTrash及支撑类——此刻都成为名为c16.trash的一个包的一部门,所以它们可以简朴地导入。
无论打开包括了Trash描写信息的数据文件,照旧对谁人文件举办理会,所有涉及到的操纵均已封装到static(静态)要领ParseTrash.fillBin()里。所以它此刻已经不是我们设计进程中要留意的一个重点。在本章剩余的部门,各人常常城市看到无论添加的是什么范例的新类,ParseTrash.fillBin()城市一连事情,不会产生改变,这无疑是一种优良的设计方案。
提到工具的建设,这一方案确实已将新范例插手系统所需的变换严格地“当地化”了。但在利用RTTI的进程中,却存在着一个严重的问题,这里已明晰地显暴露来。措施外貌上事情得很好,但却永远侦测到不能“硬纸板”(Cardboard)这种新的废品范例——纵然列内外确实有一个硬纸板范例!之所以会呈现这种环境,完全是由于利用了RTTI的缘故。RTTI只会查找那些我们汇报它查找的对象。RTTI在这里错误的用法是“系统中的每种范例”都举办了测试,而不是仅测试一种范例可能一个范例子集。正如各人今后会看到的那样,在测试每一种范例时可换用其他方法来运用多形性特征。但如果以这种形式过多地利用RTTI,并且又在本身的系统里添加了一种新范例,很容易就会健忘在措施里作出适当的窜改,从而埋下今后难以发明的Bug。因此,在这种环境下制止利用RTTI是很有须要的,这并不只仅是为了外貌悦目——也是为了发生更易维护的代码。