继承优于标签类型

开发者有时会遇到这样的类:其对象有多种不同"类别",每一"类别"的对象都用一个特定的标签来识别。比如下面的类,可以代表"圆圈"或"矩形":

// 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,指那些与核心功能无关,到处重复使用,但又不可或缺的代码),包括枚举声明、标签属性和switch块。
2. 由于多个实现被塞进同一个类中,代码可读性很差。
3. 对于任何一个特定"类别"的对象,其他"类别"独有的属性也都需要被初始化,所以内存开销很大。
4. 属性初始化依赖于构造函数的执行,所以不能声明为final。这会导致更多boilerplate的产生。
5. 构造函数必须正确初始化各"类别"相关的属性,无法获得编译器的辅助。
6. 如果要添加任何新"类别",必须通过修改代码来完成,并且必须确保更新所有的switch语句。
7. 类型本身无法提供任何关于"类别"的信息。

可见这种基于标签的类型定义是繁琐,易出错,而且低效的。很明显,处理上述类型定义需求的最好方式,应该是继承机制。本质上说,上面例子中的标签类型,是对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)。

总之,标签类型是一种错误的类型设计,必须被继承架构取代。