偶爾,你可能遇見一個(gè)類译红,它的實(shí)例有兩個(gè)或者更多的特點(diǎn)(flavor),而且包含了一個(gè)標(biāo)簽(tag)域表明這個(gè)實(shí)例的特點(diǎn)兴溜。比如侦厚,考慮如下類,它可以代表一個(gè)圓形或者長方形:
// 標(biāo)簽類 - 大大次于類層級(jí)!
class Figure { enum Shape { RECTANGLE, CIRCLE };
// 標(biāo)簽域 - 這個(gè)圖形的形狀
final Shape shape;
// 這些域僅僅當(dāng)形狀是RECTANGLE時(shí)使用
double length;
double width;
// 這個(gè)域僅僅當(dāng)形狀是CIRCLE時(shí)使用
double radius;
// 圓形的構(gòu)造子
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// 長方形的構(gòu)造子
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);
}
}
}
這樣的標(biāo)簽類有許多缺點(diǎn)拙徽。它們堆滿了樣板代碼刨沦,包括enum聲明、標(biāo)簽域和switch語句膘怕。因?yàn)槎鄠€(gè)實(shí)現(xiàn)雜亂混合在單個(gè)類中想诅,所以進(jìn)一步損害了可讀性。因?yàn)閷?shí)例承擔(dān)著屬于其他特點(diǎn)的不相關(guān)域岛心,所以內(nèi)存占用增加了来破。域不能夠變成final,除非構(gòu)造子初始化不相關(guān)域忘古,這導(dǎo)致了更多的樣板代碼徘禁。構(gòu)造子必須設(shè)置標(biāo)簽域,而且在沒有編譯器協(xié)助下初始化正確的數(shù)據(jù)域:如果你初始化了錯(cuò)誤域髓堪,那么這個(gè)程序?qū)?huì)在運(yùn)行時(shí)失敗送朱。你不能夠添加新的特點(diǎn)到一個(gè)標(biāo)簽類,除非你改變這個(gè)源文件干旁。如果你確實(shí)要添加一個(gè)特點(diǎn)驶沼,你必須記得添加一個(gè)case到每個(gè)switch語句,否則這個(gè)類將會(huì)在運(yùn)行時(shí)失敗争群。最后商乎,實(shí)例的數(shù)據(jù)類型沒有表明它的特點(diǎn)。簡而言之祭阀,標(biāo)簽類是冗長的鹉戚、容易出錯(cuò)的和低效的。
幸運(yùn)的是专控,像Java這樣的面向?qū)ο笳Z言提供了一個(gè)更好的替代方法抹凳,定義可以代表多個(gè)特點(diǎn)的單個(gè)數(shù)據(jù)類型:子類型化。標(biāo)簽類只是類型層級(jí)的蒼白的模仿伦腐。
為了把標(biāo)簽類變成類層級(jí)赢底,首先定義一個(gè)抽象類,它包含了為標(biāo)簽類中每個(gè)方法的抽象方法柏蘑,這個(gè)標(biāo)簽類行為依賴于標(biāo)簽值幸冻。在Figure類中,只有一個(gè)這樣的方法咳焚,即area洽损。這個(gè)抽象類是類層級(jí)的根。如果有任何方法的行為不依賴于標(biāo)簽值革半,把該方法放入到這個(gè)類碑定。相似地,如果所有特點(diǎn)使用的任何數(shù)據(jù)域又官,把它們放到這個(gè)類延刘。Figure類中沒有這樣的獨(dú)立于特點(diǎn)的方法或者域。
其次六敬,為原來的標(biāo)簽類的每個(gè)特點(diǎn)碘赖,定義一個(gè)根類的具體子類。在我們的例子中外构,有兩個(gè):圓形和長方形普泡。在每個(gè)子類中包含特屬于這個(gè)特點(diǎn)的數(shù)據(jù)域。在我們的例子中典勇,radius特屬于圓形劫哼,length和width屬于長方形。而且包含根類中每個(gè)抽象方法的恰當(dāng)實(shí)現(xiàn)割笙。以下是相應(yīng)于原來Figure類的類層級(jí):
// 標(biāo)簽類的類層級(jí)替代
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;
}
}
這個(gè)類層級(jí)更正了前面陳述的標(biāo)簽類的每個(gè)缺點(diǎn)权烧。這個(gè)代碼是簡單和明確的,沒有包含原版里面的樣板代碼伤溉。每個(gè)特點(diǎn)的實(shí)現(xiàn)分配了它自己的類般码,這些類沒有負(fù)擔(dān)不相關(guān)數(shù)據(jù)域。所有的域是final的乱顾。編譯器保證了每個(gè)類的構(gòu)造子初始化了它的數(shù)據(jù)域板祝,而且保證了每個(gè)類為根類中聲明的每個(gè)抽象方法有一個(gè)實(shí)現(xiàn)。這消除了由于缺少switch的case運(yùn)行時(shí)失敗的可能性走净。多個(gè)程序員可以獨(dú)立地和共同地?cái)U(kuò)展這個(gè)層級(jí)券时,而不需要訪問根類的源代碼孤里。每個(gè)特點(diǎn)相關(guān)聯(lián)的單獨(dú)數(shù)據(jù)類型,讓程序員表明這個(gè)變量的特點(diǎn)橘洞,而且限制變量和輸入?yún)?shù)到指定特點(diǎn)捌袜。
類層級(jí)的另外一個(gè)優(yōu)點(diǎn)在于,它們變得反應(yīng)了類型之間的自然層級(jí)關(guān)系炸枣,使得有更好的靈活性和更好的編譯時(shí)類型檢查虏等。假設(shè)原來例子中的標(biāo)簽類也允許正方形。這個(gè)類層級(jí)可以變成反應(yīng)這個(gè)事實(shí):正方形是長方形的特殊類型(假設(shè)兩者都是不可變的):
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}
需要注意的是适肠,上面層級(jí)中的域是直接訪問的霍衫,而不是通過訪問器方法。這是為了簡單起見侯养,如果層級(jí)是公開的敦跌,那么應(yīng)該是一個(gè)糟糕的設(shè)計(jì)(條目16)。
總之沸毁,標(biāo)簽類很少是適合的峰髓。如果你傾向于編寫有明顯標(biāo)簽域的類,那么考慮這個(gè)標(biāo)簽是否移除息尺,而且這個(gè)類是否可以用層級(jí)替代携兵。當(dāng)你遇見一個(gè)已經(jīng)存在的帶有標(biāo)簽域的類,考慮把它重構(gòu)為一個(gè)層級(jí)搂誉。