偶尔,你可能遇见一个类,它的实例有两个或者更多的特点(flavor),而且包含了一个标签(tag)域表明这个实例的特点。比如,考虑如下类,它可以代表一个圆形或者长方形:
// 标签类 - 大大次于类层级!
class Figure { enum Shape { RECTANGLE, CIRCLE };
// 标签域 - 这个图形的形状
final Shape shape;
// 这些域仅仅当形状是RECTANGLE时使用
double length;
double width;
// 这个域仅仅当形状是CIRCLE时使用
double radius;
// 圆形的构造子
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// 长方形的构造子
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError(shape);
}
}
}
这样的标签类有许多缺点。它们堆满了样板代码,包括enum声明、标签域和switch语句。因为多个实现杂乱混合在单个类中,所以进一步损害了可读性。因为实例承担着属于其他特点的不相关域,所以内存占用增加了。域不能够变成final,除非构造子初始化不相关域,这导致了更多的样板代码。构造子必须设置标签域,而且在没有编译器协助下初始化正确的数据域:如果你初始化了错误域,那么这个程序将会在运行时失败。你不能够添加新的特点到一个标签类,除非你改变这个源文件。如果你确实要添加一个特点,你必须记得添加一个case到每个switch语句,否则这个类将会在运行时失败。最后,实例的数据类型没有表明它的特点。简而言之,标签类是冗长的、容易出错的和低效的。
幸运的是,像Java这样的面向对象语言提供了一个更好的替代方法,定义可以代表多个特点的单个数据类型:子类型化。标签类只是类型层级的苍白的模仿。
为了把标签类变成类层级,首先定义一个抽象类,它包含了为标签类中每个方法的抽象方法,这个标签类行为依赖于标签值。在Figure类中,只有一个这样的方法,即area。这个抽象类是类层级的根。如果有任何方法的行为不依赖于标签值,把该方法放入到这个类。相似地,如果所有特点使用的任何数据域,把它们放到这个类。Figure类中没有这样的独立于特点的方法或者域。
其次,为原来的标签类的每个特点,定义一个根类的具体子类。在我们的例子中,有两个:圆形和长方形。在每个子类中包含特属于这个特点的数据域。在我们的例子中,radius特属于圆形,length和width属于长方形。而且包含根类中每个抽象方法的恰当实现。以下是相应于原来Figure类的类层级:
// 标签类的类层级替代
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override double area() {
return Math.PI * (radius * radius);
}
}
class Rectangle extends Figure {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override double area() {
return length * width;
}
}
这个类层级更正了前面陈述的标签类的每个缺点。这个代码是简单和明确的,没有包含原版里面的样板代码。每个特点的实现分配了它自己的类,这些类没有负担不相关数据域。所有的域是final的。编译器保证了每个类的构造子初始化了它的数据域,而且保证了每个类为根类中声明的每个抽象方法有一个实现。这消除了由于缺少switch的case运行时失败的可能性。多个程序员可以独立地和共同地扩展这个层级,而不需要访问根类的源代码。每个特点相关联的单独数据类型,让程序员表明这个变量的特点,而且限制变量和输入参数到指定特点。
类层级的另外一个优点在于,它们变得反应了类型之间的自然层级关系,使得有更好的灵活性和更好的编译时类型检查。假设原来例子中的标签类也允许正方形。这个类层级可以变成反应这个事实:正方形是长方形的特殊类型(假设两者都是不可变的):
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}
需要注意的是,上面层级中的域是直接访问的,而不是通过访问器方法。这是为了简单起见,如果层级是公开的,那么应该是一个糟糕的设计(条目16)。
总之,标签类很少是适合的。如果你倾向于编写有明显标签域的类,那么考虑这个标签是否移除,而且这个类是否可以用层级替代。当你遇见一个已经存在的带有标签域的类,考虑把它重构为一个层级。
网友评论