返回介绍

16.4.2 用于原型创建的一个范式

发布于 2024-10-15 23:56:35 字数 10953 浏览 0 评论 0 收藏 0

上述设计方案的一个问题是仍然需要一个中心场所,必须在那里知道所有类型的对象:在 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 是很有必要的,这并不仅仅是为了表面好看——也是为了产生更易维护的代码。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文