Effective Java - 慎用tagged class

版权所有,禁止匿名转载;禁止商业使用。

我不知道有没有tagged class这么一说,其实作者指的tagged class的是一个类描述了多种抽象,可以根据某个field决定不同的实例。


下面是书中例子,使用shape和部分表示长度的field构成形状并计算面积,脑补一下什么是tagged class:

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


不难看出这个类想传达什么信息,也不难看出这样的方式有很多缺陷。


虽然能看懂是什么意思,但由于各种实现挤在一个类中,其可读性并不好。


不同的实现放到一个类里描述,即会根据实现的不同而使用不同的field,即,field无法声明为final。(难道要在构造器里处理不用的field?)


虽然微不足道,内存确实存在毫无意义的占用。


不够OO。


虽然上一篇把类层次说得一无是处,其实类层次就是用来解决这一问题的,而上面的tagged class是用非OO的方式模仿类层次。


将tagged class转为类层次,首先要将tagged class里的行为抽象出来,并为其提供抽象类。


以上面的Figure为例,我们之需要一个方法——area。


接下来需要为每一个tag定义具体子类,即例子中的circle和rectangle。


然后为子类提供相应的field,即circle中的radius和rectangle的width、length。


最后为子类提供抽象方法的相应实现。


其实都不用这样去说明转换步骤,因为OO本身就是很自然的东西。


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


这样做的好处显而易见,


代码简单清晰,没有样板代码;


类型相互独立,不会受到无关field的影响,field可以声明为final。


子类行可以独立进行扩展,互不干扰。


回到最初的tagged class,它真的就一无是处?


如果使用这个类,我只需要在调用构造器时使用相应的参数就可以得到想要的实例。


就像策略模式那样。


当然,tagged class也许可能算是策略模式(传递的应该是某个行为的特征,而不是实例特征),但策略模式在Java中并不是这样使用。


通常,一个策略是通过调用者通过传递函数来指定特定的行为的。


但Java是没有函数指针的,所以我们用对象引用实现策略模式,即调用该对象的方法。


对于这种仅仅作为一个方法的"载体",即实例等同与方法指针的对象,作者将其称为函数对象(function object)。


举个例子,比如我们有这样的一个具体策略(concrete strategy):

class StringLengthComparator {
  private StringLengthComparator() {
  }
  public static final StringLengthComparator INSTANCE = new StringLengthComparator();
  public int compare(String s1, String s2) {
    return s1.length() - s2.length();
  }
}


具体策略的引用可以说是一个函数指针。


对具体策略再抽象一层即成为一个策略接口(strategy interface)。


对于上面的例子,java.util中正好有Comparator:


public interface Comparable<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
}

于是,我们使用的时候可能会用匿名类传递一个具体策略:


Arrays.sort(stringArray, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

用代码描述似乎是那么回事,我只是在我想使用的地方传递了一个具体策略。


缺点很明显——每次调用的时候又需要创建一个实例。


但又能怎么做? 把每个可能用到的具体策略在某个Host类里实现策略接口后都做成field并用final声明?


感觉很傻,但确实可以考虑,因为这样做还有其他好处。


比如,相比匿名类,具体策略可以有更直观的命名,而且具体策略可以实现其他接口。


// Exporting a concrete strategy
class Host {
  private static class StrLenCmp
    implements Comparator<String>, Serializable {
      public int compare(String s1, String s2) {
        return s1.length() - s2.length();
      }
  }
  // Returned comparator is serializable
  public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
  ... // Bulk of class omitted
}


0 0