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)。
總之,標籤類很少是適合的。如果你傾向於編寫有明顯標籤域的類,那麼考慮這個標籤是否移除,而且這個類是否可以用層級替代。當你遇見一個已經存在的帶有標籤域的類,考慮把它重構為一個層級。