首页 > 代码库 > Effective Java - 慎用tagged class

Effective Java - 慎用tagged class

作者的原标题是<Prefer class hierarchies to tagged classes>,即用类层次优于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 strategyclass 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}

Effective Java - 慎用tagged class