继承优于标签类型
开发者有时会遇到这样的类:其对象有多种不同"类别",每一"类别"的对象都用一个特定的标签来识别。比如下面的类,可以代表"圆圈"或"矩形":
// Tagged class - vastly inferior to a class hierarchy! class Figure { enum Shape { RECTANGLE, CIRCLE }; // Tag field - the shape of this figure final Shape shape; // These fields are used only if shape is RECTANGLE double length; double width; // This field is used only if shape is CIRCLE double radius; // Constructor for circle Figure(double radius) { shape = Shape.CIRCLE; this.radius = radius; } // Constructor for rectangle 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(); } } }
这样的设计有很多缺陷:
1. 满是"样板代码"(boilerplate,指那些与m88体育核心功能无关,到处重复使用,但又不可或缺的代码),包括枚举声明、标签属性和switch块。
2. 由于多个实现被塞进同一个类中,代码可读性很差。
3. 对于任何一个特定"类别"的对象,其他"类别"独有的属性也都需要被初始化,所以内存开销很大。
4. m88体育属性初始化依赖于构造函数的执行,所以不能声明为final。这会导致更多boilerplate的产生。
5. 构造函数必须正确初始化各"类别"相关的属性,无法获得编译器的辅助。
6. m88体育如果要添加任何新"类别",必须通过修改代码来完成,并且必须确保更新所有的switch语句。
7. m88体育类型本身无法提供任何关于"类别"的信息。
可见这种基于标签的类型定义是繁琐,易出错,而且低效的。很明显,处理上述类型定义需求的最好方式,应该是继承机制。本质上说,上面例子中的标签类型,是对OO语言中子类的拙劣模仿。下面是用子类实现的上例的数据类型:
// Class hierarchy replacement for a tagged class abstract class Figure { abstract double area(); } class Circle extends Figure { final double radius; Circle(double radius) { this.radius = radius; } 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; } double area() { return length * width; } }
这种基于继承的设计可以避免上面提到的标签类型的所有缺陷。而且继承链可以更清楚地反映类型之间的关系,有更好的灵活性和编译阶段的类型验证。比如,假设上面的标签类型同样可以处理"正方形"这个类别,继承链则可以表明"正方形"只是一种特殊的"矩形":
class Square extends Rectangle { Square(double side) { super(side, side); } }
为简洁起见,这个例子重用了父类的属性,如果整个继承链是开放为public的,则应避免这种用法(item 14)。
总之,标签类型是一种错误的类型设计,必须被继承架构取代。