Effective Java 3rd 条目23 类层级优于标签类

偶尔,你可能遇见一个类,它的实例有两个或者更多的特点(flavor),而且包含了一个标签(tag)域表明这个实例的特点。比如,考虑如下类,它可以代表一个圆形或者长方形:

// 标签类 - 大大次于类层级! 
class Figure { enum Shape { RECTANGLE, CIRCLE };
    // 标签域 - 这个图形的形状 
    final Shape shape;

    // 这些域仅仅当形状是RECTANGLE时使用
    double length; 
    double width;


    // 这个域仅仅当形状是CIRCLE时使用
    double radius;


    // 圆形的构造子 
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius; 
    }


    // 长方形的构造子 
    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,除非构造子初始化不相关域,这导致了更多的样板代码。构造子必须设置标签域,而且在没有编译器协助下初始化正确的数据域:如果你初始化了错误域,那么这个程序将会在运行时失败。你不能够添加新的特点到一个标签类,除非你改变这个源文件。如果你确实要添加一个特点,你必须记得添加一个case到每个switch语句,否则这个类将会在运行时失败。最后,实例的数据类型没有表明它的特点。简而言之,标签类是冗长的、容易出错的和低效的

幸运的是,像Java这样的面向对象语言提供了一个更好的替代方法,定义可以代表多个特点的单个数据类型:子类型化。标签类只是类型层级的苍白的模仿

为了把标签类变成类层级,首先定义一个抽象类,它包含了为标签类中每个方法的抽象方法,这个标签类行为依赖于标签值。在Figure类中,只有一个这样的方法,即area。这个抽象类是类层级的根。如果有任何方法的行为不依赖于标签值,把该方法放入到这个类。相似地,如果所有特点使用的任何数据域,把它们放到这个类。Figure类中没有这样的独立于特点的方法或者域。

其次,为原来的标签类的每个特点,定义一个根类的具体子类。在我们的例子中,有两个:圆形和长方形。在每个子类中包含特属于这个特点的数据域。在我们的例子中,radius特属于圆形,length和width属于长方形。而且包含根类中每个抽象方法的恰当实现。以下是相应于原来Figure类的类层级:

// 标签类的类层级替代 
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运行时失败的可能性。多个程序员可以独立地和共同地扩展这个层级,而不需要访问根类的源代码。每个特点相关联的单独数据类型,让程序员表明这个变量的特点,而且限制变量和输入参数到指定特点。

类层级的另外一个优点在于,它们变得反应了类型之间的自然层级关系,使得有更好的灵活性和更好的编译时类型检查。假设原来例子中的标签类也允许正方形。这个类层级可以变成反应这个事实:正方形是长方形的特殊类型(假设两者都是不可变的):

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

需要注意的是,上面层级中的域是直接访问的,而不是通过访问器方法。这是为了简单起见,如果层级是公开的,那么应该是一个糟糕的设计(条目16)。

总之,标签类很少是适合的。如果你倾向于编写有明显标签域的类,那么考虑这个标签是否移除,而且这个类是否可以用层级替代。当你遇见一个已经存在的带有标签域的类,考虑把它重构为一个层级。

猜你喜欢

转载自blog.csdn.net/tigershin/article/details/79373673
今日推荐