Effective Java笔记:类层次优于标签类
“类层次优于标签类” 是一个重要的面向对象设计原则,它旨在帮助我们更好地设计具有清晰职责和良好扩展性的系统结构。这个原则指出,相比使用 “标签类”(Tagged Class),通过面向对象的继承机制构建合理的类层次结构,能够让代码更加清晰、直观和易于维护。
什么是标签类(Tagged Class)?
标签类(Tagged Class) 是一种反面设计模式,通常通过一个单一的类表示多种逻辑和行为,并使用“标签字段”来区分每种逻辑。例如:
public class Figure {enum Shape { RECTANGLE, CIRCLE }; // 标签字段,标识形状的类型final Shape shape;// 用于矩形的属性double length;double width;// 用于圆形的属性double radius;// 构造圆public Figure(double radius) {shape = Shape.CIRCLE;this.radius = radius;}// 构造矩形public Figure(double length, double width) {shape = Shape.RECTANGLE;this.length = length;this.width = width;}// 计算面积(根据标签字段的类型分支处理)public double area() {switch (shape) {case RECTANGLE:return length * width;case CIRCLE:return Math.PI * radius * radius;default:throw new AssertionError(shape);}}
}
在这个例子中,Figure
使用一个 Shape
标签字段来标记它是一个矩形还是一个圆形,然后在构造器和方法中使用 switch
或 if-else
的判断逻辑来操作不同的字段。
标签类设计的缺陷
-
职责不单一:
Figure
试图通过单个类表达多个形状,每种形状都有自己的字段和逻辑,导致职责混乱。- 一个形状是矩形,另一个是圆形,它们的行为和数据完全不同,这种设计违背了单一职责原则(SRP - Single Responsibility Principle)。
-
易出错,难以维护:
- 很容易出现与逻辑无关的状态。比如,对于矩形,
radius
字段是没有意义的,但它仍然可以被实例化。 - 未来如果添加新的形状(如三角形),需要集中修改标签字段和所有相关的逻辑分支(如
switch
和if
判断),风险会随代码规模的增长而增加。
- 很容易出现与逻辑无关的状态。比如,对于矩形,
-
扩展困难:
- 如果需要添加新的形状,比如三角形,不仅要修改
Shape
枚举,还要对所有使用到shape
标签字段的地方进行修改,特别是分支判断,这会违反 “开放-封闭原则(OCP)”。
- 如果需要添加新的形状,比如三角形,不仅要修改
-
浪费内存或语义模糊:
- 不同形状分别使用不同字段,但在内存中保存了所有字段,这种冗余不仅浪费资源,还可能在读取时混淆语义。
类层次结构优于标签类
通过 类层次结构 的设计思想,我们可以分离出不同逻辑的职责,让代码更加清晰、扩展更加方便、行为更加直观。
重构案例:用继承实现类层次结构
我们可以将上述 Figure
类重构为一个基类和两个子类:
// 抽象基类,定义公共行为
public abstract class Figure {public abstract double area();
}// 矩形类
public class Rectangle extends Figure {private final double length;private final double width;public Rectangle(double length, double width) {this.length = length;this.width = width;}@Overridepublic double area() {return length * width;}
}// 圆形类
public class Circle extends Figure {private final double radius;public Circle(double radius) {this.radius = radius;}@Overridepublic double area() {return Math.PI * radius * radius;}
}
现在,每个形状的行为封装在独立的类中,Rectangle
和 Circle
专注于自己的逻辑,避免代码的混乱和重复。
标签类 vs 类层次结果的比较
特性 | 标签类 | 类层次结构 |
---|---|---|
职责单一性 | 一个类试图表示所有形状,职责混乱 | 每个子类专注自己的行为与属性 |
可扩展性 | 添加新形状需要修改多处代码 | 添加新子类只需实现抽象方法 |
易维护性 | 分支判断逻辑遍布代码 | 多态减少了显式分支判断 |
错误风险 | 无意义的字段始终存在,如矩形的 radius | 无冗余字段,语义更清晰 |
内存使用 | 保存所有字段,浪费内存 | 每个类仅保存与其相关的字段 |
多态的强大:通过类层次简化业务代码
多态是类层次结构的核心优势,我们可以用多态方法简化对形状的操作,而不需要 switch
或 if
分支。
示例:统一调用 area
方法
public static void main(String[] args) {Figure rectangle = new Rectangle(4, 5);Figure circle = new Circle(3);System.out.println("Rectangle Area: " + rectangle.area());System.out.println("Circle Area: " + circle.area());
}
- 在这个例子中,我们不需要关心
Figure
的具体类型,通过多态(area()
方法的动态绑定),自动调用各自子类的实现。这是设计良好的类层次结构的一个重要特点——消除显式的类型检查。
总结:类层次优于标签类的设计原则
1. 遵循单一职责原则
- 每个类只专注于自身的行为和逻辑,避免混杂多个职责。
Rectangle
管理矩形的逻辑和属性,Circle
管理圆形的逻辑和属性。
2. 遵循开放-封闭原则
- 使用类层次结构添加新功能时,不需要修改现有类,而是通过添加新子类来扩展行为。例如,为图形系统新增
Triangle
(三角形)时,只需增加一个继承Figure
的Triangle
类即可。
示例:为类层次结构添加新形状 Triangle
:
public class Triangle extends Figure {private final double base;private final double height;public Triangle(double base, double height) {this.base = base;this.height = height;}@Overridepublic double area() {return 0.5 * base * height;}
}
3. 减少代码中的分支判断
- 分支判断(如
switch
或if-else
)可能造成代码冗长且难以维护,与其通过标签字段传递逻辑,不如利用多态直接分发行为。
4. 提高内存效率
- 类层次结构中的每个类只包含自身的字段,避免了标签类设计中同时保存多个无用字段的资源浪费。
5. 增强类型系统的安全性
- 类层次结构强迫用户提供类型安全的构造。
- 基类抽象方法可以确保每个子类都必须实现特定的行为,符合多态的契约,这种强制性提升了类型安全性。
什么时候使用标签类?
虽然标签类通常不是设计的最佳选择,但在某些特定情况下,它依然有存在的意义:
-
当类的类型分支很少,且未来扩展几乎不可能时:
- 如果
Shape
只有两种类型(矩形和圆形),且不打算扩展为复杂的层次模型,那么标签类的设计是可以接受的。
- 如果
-
当所建类是值对象且不可变时:
- 标签类在某些轻量级的不可变类(如
enum
类型)中可能是一个简单的解决方案。
- 标签类在某些轻量级的不可变类(如
最终总结:类层次优于标签类
如果一个类需要根据 “标签字段” 来调整逻辑,那通常可以被更好的设计替代。例如:
- 用 类层次结构 构建不同行为的类别;
- 用 多态 取代分支判断。