Effective Java 3rd 條目2 面臨眾多構(gòu)造子參數(shù)時考慮builder

靜態(tài)工廠和構(gòu)造子都有一個限制:它們不能很好調(diào)節(jié)大量可選參數(shù)≈褡幔考慮這個情況敬飒,一個類表示包裝食品上的營養(yǎng)成分標(biāo)簽。這些標(biāo)簽有些是必需的域鬼佣,食用份量驶拱、份數(shù)和每份卡路里數(shù);還有二十多個可選域晶衷,總脂肪含量、飽和脂肪含量阴孟、反脂肪含量晌纫、膽固醇含量,鈉含量等等永丝。大多數(shù)產(chǎn)品中這些可選參數(shù)只有很少的非零值锹漱。

對于這樣的類,你打算用什么類型的構(gòu)造子或者靜態(tài)工廠慕嚷?習(xí)慣上哥牍,編程人員用重疊構(gòu)造子(telescoping constructor)模式:提供僅僅必需參數(shù)的構(gòu)造子,有單個可選參數(shù)的另外一個構(gòu)造子喝检,和有兩個可選參數(shù)的第三個構(gòu)造子嗅辣,等等,最后有所有可選參數(shù)的構(gòu)造子挠说。這是實(shí)踐中的樣子澡谭。為了簡單起見,只展示四個可選參數(shù):

// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
    private final int servingSize;  // (mL)            required
    private final int servings;     // (per container) required
    private final int calories;     // (per serving)   optional
    private final int fat;          // (g/serving)     optional
    private final int sodium;       // (mg/serving)    optional
    private final int carbohydrate; // (g/serving)     optional

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }
    public NutritionFacts(int servingSize, int servings,
            int calories) {
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }

    public NutritionFacts(int servingSize, int servings,
           int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize  = servingSize;
        this.servings     = servings;
        this.calories     = calories;
        this.fat          = fat;
        this.sodium       = sodium;
        this.carbohydrate = carbohydrate;
    }
}

當(dāng)你想創(chuàng)建一個實(shí)例损俭,用包含你想設(shè)置所有參數(shù)的最短參數(shù)列表的構(gòu)造子:

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

通常蛙奖,調(diào)用這樣的構(gòu)造子需要你不想設(shè)置的參數(shù),但是你被迫為它們傳遞參數(shù)杆兵。這種情況下雁仲,你傳遞0這個值給fat。對于“只有”六個參數(shù)的情況琐脏,這個看上去不算很糟攒砖,但是當(dāng)參數(shù)數(shù)量增加時,很快就會失控。

簡而言之祭衩,重疊構(gòu)造子模式奏效灶体,但是當(dāng)有很多參數(shù)的時候,寫客戶端代碼是困難的掐暮,讀代碼更困難蝎抽。閱讀的人會想,這些值是什么路克,而且必須細(xì)心計(jì)算參數(shù)來弄明白樟结。相同類型參數(shù)的長長的系列會造成微妙的bug。如果客戶端不慎顛倒了兩個這樣的參數(shù)精算,編譯器不會發(fā)現(xiàn)瓢宦,但是程序在運(yùn)行時異常(條目51)。

當(dāng)面臨許多可選參數(shù)的構(gòu)造子時灰羽,第二個替代方案是JavaBean模式驮履,這個模式中,你調(diào)用一個無參數(shù)的構(gòu)造子來創(chuàng)建一個對象廉嚼,然后調(diào)用設(shè)置方法來設(shè)置每個必需的參數(shù)和感興趣的可選參數(shù):

// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
    // Parameters initialized to default values (if any)
    private int servingSize  = -1; // Required; no default value
    private int servings     = -1; // Required; no default value
    private int calories     = 0;
    private int fat          = 0;
    private int sodium       = 0;
    private int carbohydrate = 0;

    public NutritionFacts() { }
    // Setters
    public void setServingSize(int val)  { servingSize = val; }
    public void setServings(int val)    { servings = val; }
    public void setCalories(int val)    { calories = val; }
    public void setFat(int val)         { fat = val; }
    public void setSodium(int val)      { sodium = val; }
    public void setCarbohydrate(int val) { carbohydrate = val; }
}

這個模式?jīng)]有重疊構(gòu)造子模式的缺點(diǎn)玫镐。雖然有點(diǎn)啰嗦,但是容易創(chuàng)建實(shí)例怠噪,代碼也容易閱讀:

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

不幸的是恐似,JavaBean模式有它自己的嚴(yán)重缺點(diǎn)。因?yàn)闃?gòu)造被分成多個調(diào)用傍念,一個JavaBean在構(gòu)造的過程中有可能不一致的狀態(tài)矫夷。這個類沒有這個選擇:檢查構(gòu)造子參數(shù)的有效性來實(shí)行一致性。嘗試使用一個不一致狀態(tài)的對象憋槐,會導(dǎo)致失敗双藕,這個失敗與含有bug的代碼大相徑庭,所以難于調(diào)試秦陋。一個相應(yīng)的缺點(diǎn)是蔓彩,JavaBean模式限制了生成一個不變類的可能性,需要編碼者花額外的功夫保證線性安全驳概。
手動“凍結(jié)(freezing)”對象來減少這些缺點(diǎn)是可能的:當(dāng)對象構(gòu)造完成了赤嚼,不允許使用直到凍結(jié)。但是這個變體是笨拙的顺又,在實(shí)踐中用的非常稀少更卒。此外,在運(yùn)行中會造成錯誤稚照,因?yàn)樵谑褂盟磅蹇眨幾g器不能保證編碼者調(diào)用一個對象的凍結(jié)方法俯萌。
幸運(yùn)的是,有第三種選擇上枕,可以結(jié)合重疊構(gòu)造子模式的安全性和JavaBean模式的可讀性咐熙。這個是Builder模式[Gamma95]的一種形式。不是直接生成對象辨萍,而是客戶端調(diào)用有所有必需參數(shù)的一個構(gòu)造子(或者靜態(tài)工廠)棋恼,獲得一個builder對象。然后客戶端在builder對象上調(diào)用類似設(shè)置方法锈玉,分別設(shè)置感興趣的可選參數(shù)爪飘。最后,客戶端調(diào)用無參數(shù)的build方法來生成對象拉背,這個對象一般是不可變的师崎。builder是一個它構(gòu)建的類的靜態(tài)成員類(條目24)。下面是在實(shí)踐中的樣子:

// Builder Pattern
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private int calories      = 0;
        private int fat           = 0;
        private int sodium        = 0;
        private int carbohydrate  = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }

        public Builder calories(int val)
            { calories = val;      return this; }
        public Builder fat(int val)
            { fat = val;           return this; }
        public Builder sodium(int val)
            { sodium = val;        return this; }
        public Builder carbohydrate(int val)
            { carbohydrate = val;  return this; }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

NutritionFacts類是不變的椅棺,所有參數(shù)的默認(rèn)值在一個地方犁罩。builder的設(shè)置方法返回builder自身,可以鏈?zhǔn)秸{(diào)用两疚,所以有流暢的API昼汗。下面是客戶端代碼的樣子:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
        .calories(100).sodium(35).carbohydrate(27).build();

客戶端代碼容易寫,更重要的是也容易通讀鬼雀。builder模式模仿了在Python和Scala中有名字的可選參數(shù)

為了簡單蛙吏,省略了有效性檢查源哩。為了盡快發(fā)現(xiàn)無效的參數(shù),在builder構(gòu)造子和方法中檢查參數(shù)的有效性鸦做。build方法調(diào)用的有多個參數(shù)的構(gòu)造子励烦,檢查這些不變量。為了保證這些不變量不受攻擊泼诱,在拷貝builder的參數(shù)后(條目50)坛掠,務(wù)必對對象的域進(jìn)行檢查。如果檢查失敗治筒,拋出IllegalArgumentException屉栓,它的具體信息表示哪些參數(shù)是無效的(條目75)。

builder模式非常適合類繼承耸袜。使用并行的builder的層級友多,每個builder嵌套在對應(yīng)的類中。抽象的類有抽象的builder堤框;具體的類有具體的builder域滥。比如纵柿,考慮一個抽象類是代表各種種類披薩的層級的根類:

// Builder pattern for class hierarchies
public abstract class Pizza {
   public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
   final Set<Topping> toppings;

   abstract static class Builder<T extends Builder<T>> {
      EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
      public T addTopping(Topping topping) {
         toppings.add(Objects.requireNonNull(topping));
         return self();
      }

      abstract Pizza build();

      // Subclasses must override this method to return "this"
      protected abstract T self();
   }
   Pizza(Builder<?> builder) {
      toppings = builder.toppings.clone(); // See Item  50
   }
}

注意,Pizza.Builder是一個有遞歸類型參數(shù)(recursive type parameter)(條目30)的泛型(generic type)启绰。這個和抽象的self方法一起昂儒,可以在子類中使得鏈?zhǔn)椒椒ㄗ嘈В恍枰獜?qiáng)行轉(zhuǎn)換委可。Java缺少self類型渊跋,這個變通方案被認(rèn)為是模擬self類型的慣例。

Pizza有兩個具體的子類撤缴,一個是紐約類型的比薩刹枉,另外一個是半月比薩。前者有指定大小的參數(shù)屈呕,而后者需要指定醬汁是在外面還是里面微宝。

public class NyPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override public NyPizza build() {
            return new NyPizza(this);
        }

        @Override protected Builder self() { return this; }
    }

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

public class Calzone extends Pizza {
    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauceInside = false; // Default

        public Builder sauceInside() {
            sauceInside = true;
            return this;
        }

        @Override public Calzone build() {
            return new Calzone(this);
        }

        @Override protected Builder self() { return this; }
    }

    private Calzone(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }
}

注意, 每個子類builder中的build方法是聲明返回正確的子類的:NyPizza.Builderde的build方法返回NyPizza虎眨,而Calzone.Builder的build方法返回Calzone蟋软。這個技巧,一個子類方法被聲明返回一個聲明在超類中返回類型的子類型嗽桩,叫做協(xié)變返回類型(covariant return typing)岳守。這些層級builder的客戶端代碼,本質(zhì)上與簡單的NutritionFacts builder代碼是等同的碌冶。

客戶端的代碼例子如下湿痢,為了簡潔,假設(shè)enum常量是靜態(tài)導(dǎo)入:

NyPizza pizza = new NyPizza.Builder(SMALL)
        .addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
        .addTopping(HAM).sauceInside().build();

相對于構(gòu)造子扑庞,builder有個小優(yōu)點(diǎn)是譬重,builder可以有多個可變參數(shù),因?yàn)槊總€參數(shù)是它自己方法中指定的罐氨⊥喂妫或者,build可以把傳入到多次調(diào)用的方法的參數(shù)聚合到單個域栅隐,就像前面addTopping方法展示的一樣塔嬉。

builder模式相當(dāng)靈活。單個builder可以重復(fù)使用到構(gòu)建多個對象租悄。隨著創(chuàng)建對象的不同谨究,builder的參數(shù)在build方法的調(diào)用之間可以改變。對象創(chuàng)建時builder可以自動填充到域中恰矩,比如隨著每次創(chuàng)建時一個增加的系列數(shù)记盒。

builder模式也有缺點(diǎn),為了創(chuàng)建一個對象外傅,必須首先創(chuàng)建它的builder纪吮。創(chuàng)建builder的代價在實(shí)踐中雖然是不顯而易見俩檬,但是在性能要求苛刻的情形中是一個問題。而且碾盟,builder模式相對于重疊構(gòu)造子模式更加啰嗦棚辽,所以如果有足夠的參數(shù),比如四個或者更多冰肴,才值得使用屈藐。但是記住,未來你有可能添加更多的參數(shù)熙尉。但是联逻,如果你開始使用構(gòu)造子或者靜態(tài)工廠,然后當(dāng)類演變到參數(shù)個數(shù)失控的時候检痰,切換到builder包归,這時廢棄的構(gòu)造子或者靜態(tài)工廠一直存在,這是個尷尬的事情铅歼。

總之公壤,在設(shè)計(jì)類時,當(dāng)它的構(gòu)造子或者靜態(tài)工廠有多過一些參數(shù)的時候椎椰,builder模式是一個好的選擇厦幅,特別是當(dāng)多個參數(shù)是可選的或者是同個類型的時候。和重疊構(gòu)造子相比慨飘,客戶端代碼更容易閱讀和編寫确憨,而且builder比JavaBeans更安全怀泊。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末壁涎,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子趟章,更是在濱河造成了極大的恐慌堤瘤,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,865評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浆熔,死亡現(xiàn)場離奇詭異本辐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)医增,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評論 3 399
  • 文/潘曉璐 我一進(jìn)店門慎皱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人叶骨,你說我怎么就攤上這事茫多。” “怎么了忽刽?”我有些...
    開封第一講書人閱讀 169,631評論 0 364
  • 文/不壞的土叔 我叫張陵天揖,是天一觀的道長夺欲。 經(jīng)常有香客問我,道長今膊,這世上最難降的妖魔是什么些阅? 我笑而不...
    開封第一講書人閱讀 60,199評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮斑唬,結(jié)果婚禮上市埋,老公的妹妹穿的比我還像新娘。我一直安慰自己恕刘,他們只是感情好缤谎,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,196評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著褐着,像睡著了一般坷澡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上献起,一...
    開封第一講書人閱讀 52,793評論 1 314
  • 那天洋访,我揣著相機(jī)與錄音,去河邊找鬼谴餐。 笑死姻政,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的岂嗓。 我是一名探鬼主播汁展,決...
    沈念sama閱讀 41,221評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼厌殉!你這毒婦竟也來了食绿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,174評論 0 277
  • 序言:老撾萬榮一對情侶失蹤公罕,失蹤者是張志新(化名)和其女友劉穎器紧,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體楼眷,經(jīng)...
    沈念sama閱讀 46,699評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铲汪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,770評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了罐柳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掌腰。...
    茶點(diǎn)故事閱讀 40,918評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖张吉,靈堂內(nèi)的尸體忽然破棺而出齿梁,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,573評論 5 351
  • 正文 年R本政府宣布勺择,位于F島的核電站创南,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏酵幕。R本人自食惡果不足惜扰藕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,255評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芳撒。 院中可真熱鬧邓深,春花似錦、人聲如沸笔刹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舌菜。三九已至萌壳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間日月,已是汗流浹背袱瓮。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留爱咬,地道東北人尺借。 一個月前我還...
    沈念sama閱讀 49,364評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像精拟,于是被迫代替她去往敵國和親燎斩。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,926評論 2 361

推薦閱讀更多精彩內(nèi)容