Effective Java 第三版——23. 优先使用类层次而不是标签类

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。程序员

Effective Java, Third Edition

23. 优先使用类层次而不是标签类

有时你可能会碰到一个类,它的实例有两个或更多的风格,而且包含一个标签属性(tag field),表示实例的风格。 例如,考虑这个类,它能够表示一个圆形或矩形:ide

// 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(shape);
        }
    }
}

这样的标签类具备许多缺点。 他们杂乱无章的样板代码,包括枚举声明,标签属性和switch语句。 可读性更差,由于多个实如今一个类中混杂在一块儿。 内存使用增长,由于实例负担属于其余风格不相关的领域。 属性不能成为final,除非构造方法初始化不相关的属性,致使更多的样板代码。 构造方法在编译器的帮助下,必须设置标签属性并初始化正确的数据属性:若是初始化错误的属性,程序将在运行时失败。 除非能够修改其源文件,不然不能将其添加到标记的类中。 若是你添加一个风格,你必须记得给每一个switch语句添加一个case,不然这个类将在运行时失败。 最后,一个实例的数据类型没有提供任何关于风格的线索。 总之,标签类是冗长的,容易出错的,并且效率低下学习

幸运的是,像Java这样的面向对象的语言为定义一个可以表示多种风格对象的单一数据类型提供了更好的选择:子类型化(subtyping)。标签类仅仅是一个类层次的简单的模仿。this

要将标签类转换为类层次,首先定义一个包含抽象方法的抽象类,该标签类的行为取决于标签值。 在Figure类中,只有一个这样的方法,就是area方法。 这个抽象类是类层次的根。 若是有任何方法的行为不依赖于标签的值,把它们放在这个类中。 一样,若是有全部的方法使用的数据属性,把它们放在这个类。Figure类中不存在这种与类型无关的方法或属性。翻译

接下来,为原始标签类的每种类型定义一个根类的具体子类。 在咱们的例子中,有两个类型:圆形和矩形。 在每一个子类中包含特定于改类型的数据字段。 在咱们的例子中,半径属性是属于圆的,长度和宽度属性都是矩形的。 还要在每一个子类中包含根类中每一个抽象方法的适当实现。 这里是对应于Figure类的类层次:设计

// 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; }

    @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语句而致使的运行时失败的可能性。 多个程序员能够独立地继承类层次,而且能够相互操做,而无需访问根类的源代码。 每种类型都有一个独立的数据类型与之相关联,容许程序员指出变量的类型,并将变量和输入参数限制为特定的类型。code

类层次的另外一个优势是可使它们反映类型之间的天然层次关系,从而提升了灵活性,并提升了编译时类型检查的效率。 假设原始示例中的标签类也容许使用正方形。 类层次能够用来反映一个正方形是一种特殊的矩形(假设它们是不可变的):对象

lass Square extends Rectangle {
    Square(double side) {
        super(side, side);
    }
}

请注意,上述层中的属性是直接访问的,而不是访问方法。 这里是为了简洁起见,若是类层次是公开的(条目16),这将是一个糟糕的设计。blog

总之,标签类不多有适用的状况。 若是你想写一个带有明显标签属性的类,请考虑标签属性是否能够被删除,而类是否被类层次替换。 当遇到一个带有标签属性的现有类时,能够考虑将其重构为一个类层次中。继承

相关文章
相关标签/搜索