当使用字段中非凡的标记来区别对象类型时,会产生标记对相关数据误贴标签的错误 ? 被称为 Impostor Type 错误模式。在诊断 Java 代码的这一部分中,Eric Allen 对这个错误的症状和起因进行了分析,具体说明了预防错误产生的方法,并讨论了一种吸引人的混合实现方法,这种方法不使用 impostor type,但最后,还是有很多相同的缺点产生。请在讨论论坛与作者和其他读者分享您对本文的看法。
程序中除了最无关紧要的部分外都要对某些数据类型进行操作。静态类型系统提供了一种方法,它能够确保程序不会对给定类型的数据进行不当的操作。Java 语言的优点之一是严格的区分类型,所以在程序运行前已消除了类型错误。作为开发人员,我们可以使用这个类型系统提供更健壮且没有错误的代码。然而,我们却经常没有让类型系统发挥出最大的潜力。
Impostor Type 错误模式
很多程序可以更多地使用静态类型系统,但它们没有这样做,而是依靠包含区别数据类型标记的非凡字段。
代码快速跟踪
清单 1. 用 impostor type 实现几何外形
一个基本示例,演示了引入这类错误是如何的轻而易举。
清单 2. 构造新的 form
引入了错误。
清单 3. 用实际类型实现 form
这种新奇的方法可以在运行时报错。
清单 4. 一种混合的实现方式
不使用 impostor type,但易受性一样。
依靠这些非凡字段区别数据类型,这样的程序放弃了类型系统专门提供给它们的保护措施。当这些标记中的一个对它的数据误贴了标签,就会产生我称之为 Impostor Type 的错误。
症状
impostor type 错误的一种常见症状是很多概念上不同类型的数据都被同样(并且错误)的方式处理。另一常见症状是数据与任何指定的类型都不匹配。
首要规则是,只要当概念上的数据类型和它被程序处理的方法不匹配,就可以怀疑是否发生了这个模式的错误。
为说明引入这种模式的错误是多么的轻而易举,让我们来考虑一个简单的示例。假设我们需要处理各种各样的欧几里得几何学外形,如圆形、正方形等等。这些几何外形没有坐标,但含有一个 scale 变量,所以可以计算它们的面积。
清单 1. 用 imposter type 实现各种几何外形
public class Form {
String shape;
double scale;
public Form(String _shape, double _scale) {
this.shape = _shape;
this.scale = _scale;
}
public double getArea() {
if (shape.equals("square")) {
return scale * scale;
}
else if (shape.equals("circle")) {
return Math.PI * scale * scale;
}
else { // shape.equals("triangle"), an equilateral triangle
return scale * (scale * Math.sqrt(3) / 4);
}
}
}
尽管您会发现人们经常这么做,但用这种方法实现几何外形还是存在严重缺点。
最显著的缺点之一是这个方法不能真正的扩展。假如要为我们的 form 引入一个新的几何外形(比如,“五边形”),我们必须进入并修改 getArea() 方法的源代码。不过可扩展性是个独立的考虑因素;在本文中,我们把重点放在实现几何外形所造成的错误的易受性上。我会在以后的文章中回到关于可扩展性的问题上来。
假如我们在程序其它部分构造了一个新的 Form 对象,如下所示,请考虑将会发生什么情况:
清单 2. 构造一个新的 form
Form f = new Form("sqaure", 2);
当然,“square”被拼错了,但是编译器认为,这是完全合法的代码。
现在考虑一下,当我们试图对新的 Form 对象调用,比如说 getArea() 方法时发生什么情况。因为 Form 对象中的几何外形与 if-then-else 代码块中的任一测试的几何外形都不匹配,它的面积将在 else 分句中被计算,似乎它是个三角形似的!
这里将不会报错。事实上,在很多情况下,返回值看起来都好象是完全合理的数字。即使我们插入些冗余代码,检查 else 分句中的隐含条件是否包含(比如说,断言),也要到代码执行时才能发现错误。
很多其它相似的错误也可能在上述代码中产生。if-then-else 代码块可能会偶然遗漏一句分句,导致类型与那句分句相对应的所有 Form 都被错误地处理了。此外,因为 impostor type 在字段中只是一个 String,所以它可能会被意外或恶意地修改。
无论用哪一种方法,这样的修改会带来各种各样的损害。
治疗和预防措施
正如您可能设想过的那样,我建议用类型系统在静态检查期间将它们清除,从而避免这种类型的错误。请考虑这种新奇的实现方法:
清单 3. 用实际类型实现 form
public abstract class Form {
double scale;
public Form(double _scale) {
this.scale = _scale;
}
public abstract double getArea();
}
class Square extends Form {
public Square(double _scale) {
super(_scale);
}
public double getArea() {
return scale * scale;
}
}
class Circle extends Form {
public Circle(double _scale) {
super(_scale);
}
public double getArea() {
return Math.PI * scale * scale;
}
}
class Triangle extends Form {
public Triangle(double _scale) {
super(_scale);
}
public double getArea() {
return scale * (scale * Math.sqrt(3) / 4);
}
}
现在考虑一下,在创建一个新 Form 时,假如误输入了“Sqaure”,会发生什么情况。编译器将会报错,告诉我们类 Sqaure 找不到。代码将连运行的机会也没有。
同样地,编译器将不会答应我们忘记为我们的任意子类定义 getArea() 方法。当然,任何对象要改变 Form 的类型是不可能的。
最后说明
在离开这个主题之前,我还想讨论另一种可能的实现,一种我曾经讨论过的两种实现方法的混合。
在这种情况下,不使用 impostor type,但代码包含很多相同的易受性,似乎它们以前就有。实际上,这种实现方法比对每个类型单独实现 getArea() 方法更差。
清单 4. 一种混合的实现方式
public abstract class Form {
double scale;
public Form(double _scale) {
this.scale = _scale;
}
public double getArea() {
if (this instanceof Square) {
return scale * scale;
}
else if (this instanceof Circle) {
return Math.PI * scale * scale;
}
else { // this instanceof Triangle
return scale * (scale * Math.sqrt(3) / 4);
}
}
}
class Square extends Form {
public Square(double _scale) {
super(_scale);
}
}
class Circle extends Form {
public Circle(double _scale) {
super(_scale);
}
}
class Triangle extends Form {
public Triangle(double _scale) {
super(_scale);
}
}
尽管编译器仍然会捕捉类型的拼写错误,且对象类型是无法改变的,我们又一次使用了 if-then-else 代码块调度适当的类型。这样,我们又要面临 if-then-else 代码块中 instanceof 检查与我们所操作的那组类型不匹配的情况。
还必须提出,像第一种实现方法那样,这个实现方法的扩展性不如第二种。
总结
那么,简而言之,这就是我们最近的错误模式:
模式:Impostor Type
症状:一种程序,它用同样的方式处理概念上不同类型的数据,或者无法识别某种类型的数据。
起因:程序针对各种类型的数据使用带标记的字段,而不是独立的类。
治疗和预防措施:尽可能将概念上不同的数据类型分成几个独立的类。
重点在于,这种语言为您提供了避免这类错误的最好资源 ? 只是要记得使用它们。
参考资料
请参与本文的讨论论坛。
JUnit 主页提供了很多有趣文章的链接,这些文章讨论了程序测试的方法,还有最新的 JUnit 版本。
假如您喜欢 JUnit,请查看整套 xUnit 测试工具,有多种不同语言版本。
我必须提一下工具中的 xUnit 套件是设计用来和极端编程一起使用的,这是一种新型的功能强大并快速开发干净、健壮软件的方法。
“框架体系结构的 UML 概要文件”(PDF 幻灯片放映)突出显示具体的 JUnit 个案研究。
尽管与这个讨论没有直接的联系,我还是推荐您参阅 Martin Fowler 的文章,其中讨论了 UML 的角色和极端编程中的设计。
采用 “Java 调试”教程(developerWorks,2001 年 2 月),从而获得一般调试技术的帮助。
不熟悉 Java 开发或希望重温 Java 编程技术吗?请采用这篇全面的教程,“ Java 语言的基础知识”。
请阅读 Eric 的所有诊断 Java 代码的文章,许多篇着重讨论错误模式。
请在 developerWorks Java 技术专区中查阅更多 Java 参考资料。
关于作者
Eric Allen 毕业于 Cornell 大学,曾获得计算机系和数学系的学士学位。他还是 Rice 大学 Java 编程语言小组的博士研究生。它的研究涉及到开发用于 Java 语言的语义模型和静态分析工具,两者都是源代码和字节码级别的。目前,他正在为 NextGen 编程语言实现一种从源代码到字节码的编译器,这也是 Java 语言的泛型运行时类型的一种扩展。请通过 eallen@cs.rice.edu 与他联系。