项目23: 设计类层次结构,而不是用标记区分

2019-05-31  本文已影响0人  rabbittttt

ITEM 23: PREFER CLASS HIERARCHIES TO TAGGED CLASSES
  有时,您可能会遇到一个类,该类的实例有两种或两种以上的风格,并且包含一个标记字段,表示实例的风格。例如,考虑这个类,它能够表示一个圆或一个矩形:

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

  这样标记的类有许多缺点。它们充满了样板文件,包括 enum 声明、标记字段和 switch 语句。可读性进一步受到损害,因为多个实现混杂在一个类中。内存占用会增加,因为实例负载着属于其他类型的不相关字段。除非构造函数初始化不相关的字段,否则不能将字段设置为 final,这会导致更多的样板文件。构造函数必须设置标记字段并在没有编译器帮助的情况下初始化正确的数据字段:如果初始化了错误的字段,程序将在运行时失败。除非能够修改带标记的类的源文件,否则不能向该类添加新的类型。如果您确实添加了新类型,则必须记住为每个switch 语句添加一个 case,否则类将在运行时失败。最后,实例的数据类型对它的类型没有任何提示。简而言之,带标记的类冗长、容易出错且效率低下。
  幸运的是,像 Java 这样的面向对象语言提供了一个更好的选择,可以定义一个能够表示多种风格对象的数据类型:子类。带标记的类只是对类层次结构的苍白模仿。
  要将带标记的类转换为类层次结构,首先要为带标记的类中的每个方法定义一个抽象类,其中包含一个抽象方法,该类的行为取决于标记值。在 Figure Class中,只有一个这样的方法,那就是 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。编译器确保每个类的构造函数初始化其数据字段,并且每个类对于根类中声明的每个抽象方法都有一个实现。这消除了由于缺少开关情况而导致运行时失败的可能性。多个程序员可以独立地、可互操作地扩展层次结构,而无需访问根类的源代码。每种风格都有一个单独的数据类型,允许程序员指出变量的风格,并将变量和输入参数限制为特定的风格。
  类层次结构的另一个优点是,它们可以反映类型之间的自然层次关系,从而提高灵活性和更好的编译时类型检查。假设原始示例中的标记类也允许使用正方形。类层次结构可以反映出正方形是一种特殊的矩形(假设两者都是不可变的):

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

  注意,上面层次结构中的字段是直接访问的,而不是通过访问器方法访问的。这样做是为了简洁,如果层次结构是公共的,这将是一个糟糕的设计(item 16)。
  总之,标记类很少是合适的选择。如果您想编写一个带有显式标记字段的类,请考虑是否可以删除标记并用层次结构替换该类。当您遇到具有标记字段的现有类时,请考虑将其重构为层次结构。

上一篇 下一篇

猜你喜欢

热点阅读