1. 程式人生 > >Effective Java 3rd 條目23 類層級優於標籤類

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)。

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