條款2:當(dāng)面對很多構(gòu)造方法參數(shù)時,請考慮使用構(gòu)建器

靜態(tài)??與構(gòu)造?法有?個共通的限制:當(dāng)存在?量的可選參數(shù)時,他們的可伸縮性很差。考慮這樣?個類,它表示貼在包裝好的?品上的營養(yǎng)表標(biāo)簽。這些標(biāo)簽有?些必要的字段,如分量??、每瓶容量以及每份的卡路?數(shù),還有20多個可選字段,如總脂肪量、飽和脂肪酸、反式脂肪酸、膽固醇及鈉元素等等。對于?多數(shù)產(chǎn)品來說,只有少量的這些可選字段有?零值。

對于這樣?個類來說,你應(yīng)該編寫哪種構(gòu)造?法或是靜態(tài)??呢?傳統(tǒng)上,程序員們會使?重疊構(gòu)造?法模式,在這種模式中,你會提供?個只接收必要參數(shù)的構(gòu)造?法,然后編寫?個接收單個可選參數(shù)的構(gòu)造?法,再編寫?個接收兩個可選參數(shù)的構(gòu)造?法,以此類推,最后提供?個接收所有可選參數(shù)的構(gòu)造?法。如下代碼示例就說明了這?點。出于簡潔的?的,這?只給出了4個可選字段:

// JavaBeans Pattern - allows inconsistency, mandates mutability
// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
  private final int servingSize; // (mL) required
  private int servings; // (per container) required
  private int calories; // (per serving) optional
  private int fat; // (g/serving) optional
  private 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;
  }
}

在創(chuàng)建實例時,你可以使?包含了所要設(shè)置的所有參數(shù)的最短參數(shù)列表構(gòu)造?法:

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

?般來說,這個構(gòu)造?法調(diào)?需要很多你并不想設(shè)置的參數(shù),但卻不得不為其傳值。在這種情況下,我們?yōu)?code>fat傳遞了0值。上述代碼『只有』6個參數(shù),看起來還不算太糟糕,不過隨著參數(shù)數(shù)量的增加,很快你就數(shù)不過來了。

??以蔽之,重疊構(gòu)造?法模式可以?,但當(dāng)參數(shù)數(shù)量過多時,客戶端代碼的編寫就變得愈發(fā)困難,閱讀起來也更費勁。閱讀者會想,所有這些值是什么意思,并且要??地檢查參數(shù)才能知道結(jié)果。??的同類型參數(shù)序列會導(dǎo)致?常隱秘的Bug。如果客戶端不??改變了這樣兩個參數(shù)的順序,那么編譯器是不知情的,不過程序在運?期則會表現(xiàn)出錯誤的?為(條款51

當(dāng)構(gòu)造?法中存在?量可選參數(shù)時,另?種解決?案是JavaBeans模式。在這種模式下,你會通過?個?參構(gòu)造?法來創(chuàng)建對象,接下來調(diào)?setter?法設(shè)置每個必填參數(shù)與所需要的每個可選參數(shù):

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;

    /*無參構(gòu)造方法*/
    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)造?法模式的缺點。通過這種?式可以輕松創(chuàng)建實例(就是稍微有點冗?),并且代碼讀起來也?較容易:

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

但遺憾的是,JavaBeans模式?身存在嚴(yán)重的缺陷。由于構(gòu)造過程被劃分為多個調(diào)?,因此JavaBean在構(gòu)造過程中可能會處于?種不?致的狀態(tài)下。類僅僅通過檢查構(gòu)造?法參數(shù)的有效性是?法確保?致性的。當(dāng)對象處于不?致狀態(tài)時,使?這個對象會導(dǎo)致難以覺察的Bug,這種Bug也極難調(diào)試。與之相關(guān)的另?個缺陷就是JavaBeans模式?法確保?個類的不變性(條款17),并且需要程序員??確保線程安全性。

當(dāng)構(gòu)造完畢時,我們可以通過??『凍結(jié)』對象并且直到凍結(jié)后才允許使?對象來消除這些缺陷,不過這種做法很少使?。此外,這么做會導(dǎo)致運?期錯誤,因為編譯器?法確保程序員在使?對象前會調(diào)?對象的凍結(jié)?法。

幸好,還有?種?案融合了重疊構(gòu)造?法模式的安全性與JavaBeans模式的可讀性。它是?種構(gòu)建器模式[Gamma95]。相?于直接創(chuàng)建所需的對象,客戶端會調(diào)??個構(gòu)造?法(或是靜態(tài)??),該構(gòu)造?法帶有所需的參數(shù),并且得到?個構(gòu)建器對象。接下來,客戶端會對構(gòu)建器對象調(diào)?類似于setter的?法來設(shè)置每?個感興趣的可選參數(shù)。最后,客戶端會調(diào)??個?參的build?法來?成對象,該對象是個不變對象。構(gòu)建器通常是它所構(gòu)建的類的?個靜態(tài)成員類(條款24)。如下代碼展示了其使??式:

public class NutritionFacts {
  private int servingSize;
  private int servings;
  private int calories;
  private int fat;
  private int sodium;
  private int carbohydrate;

    /*私有構(gòu)造方法*/
  private NutritionFacts(Builder builder){
    servingSize = builder.servingSize;
    servings = builder.servings;
    calories = builder.calories;
    fat = builder.fat;
    sodium = builder.sodium;
    carbohydrate = builder.carbohydrate;
  }
  
    /*靜態(tài)成員類  ---> 構(gòu)建器 */
  public static class Builder {
    private int servingSize;
    private int servings;
    private int calories;
    private int fat;
    private int sodium;
    private int carbohydrate;

    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){
      this.sodium = val;
      return this;
    }

    public Builder carbohydrate(int val){
      carbohydrate = val;
      return this;
    }
        /*返回要構(gòu)建的真正對象*/
    public NutritionFacts build(){
      return new NutritionFacts(this);
    }
  }
}

NutritionFacts類是不可變的,所有的參數(shù)默認值都在?個地?。構(gòu)建器的setter?法返回的是構(gòu)建器?身,這樣調(diào)?就可以鏈接起來,形成?種流式API。如下展示了客戶端代碼的樣?:

public class TestClient {
  public static void main(String[] args) {
    NutritionFacts test = new NutritionFacts.Builder(1,2).
      calories(5)
      .carbohydrate(6)
      .sodium(7)
      .fat(8)
      .build();

    System.out.println(test);
  }
}

上述客戶端代碼編寫起來很容易,更為重要的是,閱讀起來也?常輕松。構(gòu)建器模式模擬了PythonScala中的具名可選參數(shù)。

出于簡潔的?的,這?省略了有效性檢查。為了能盡快檢測出?效參數(shù),請在構(gòu)建器的構(gòu)造?法與?法中檢查參數(shù)的有效性。不變性檢查涉及到由build?法所調(diào)?的構(gòu)造?法中的多個參數(shù)。為了確保這些不變參數(shù)真正能夠做到不變,請在從構(gòu)建器中復(fù)制完參數(shù)后就對對象字段進?檢查(條款50)。如果檢查失敗,那就要拋出IllegalArgumentException異常(條款72),異常的詳細信息標(biāo)識出了哪些參數(shù)是?效的(條款75)。

構(gòu)建器模式?常適合于類繼承。使?平?的構(gòu)建器層次體系,每個都嵌套在對應(yīng)的類中。抽象類有抽象構(gòu)建器;具體類有具體構(gòu)建器。?如說,考慮如下這個抽象類,它是層次體系的根,表示各種類型的披薩:

// 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(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ù)(條款30)。通過該參數(shù)以及抽象的self?法可以讓?法在?類中恰當(dāng)?shù)劓溄悠饋恚??需進?類型轉(zhuǎn)換。這種對于Java缺乏?我類型問題的解決?案叫做模擬的?我類型。

如下是兩個具體的Pizza?類,?個代表標(biāo)準(zhǔn)的紐約?格披薩,另?個代表半圓形烤乳酪披薩。前者有?個必填的size參數(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 = 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;
  }
}

注意到每個?類構(gòu)建器中的build?法都被聲明為返回正確的?類:NyPizza.Builderbuild?法返回NyPizza,Calzone.Builderbuild?法則返回Calzone。這種?類?法聲明為返回?類?法所聲明的返回類型的?類型的技術(shù)叫做協(xié)變返回類型??蛻舳丝梢酝ㄟ^這項技術(shù)在不借助于類型轉(zhuǎn)換的情況下使?這些構(gòu)建器。

這些『層次化構(gòu)建器』的客戶端代碼本質(zhì)上與簡單的NutritionFacts構(gòu)建器客戶端代碼別??致。出于簡潔的?的,如下的示例客戶端代碼假設(shè)已經(jīng)靜態(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)造?法來說,構(gòu)建器的?個??的優(yōu)勢在于構(gòu)建器可以擁有多個可變參數(shù),這是因為每個參數(shù)都是在??的?法中指定的。此外,構(gòu)建器可以將傳遞給多個調(diào)?的參數(shù)聚合起來,并通過?個?法傳給單個字段,這?點在之前的addTopping?法中已經(jīng)介紹過了

構(gòu)建器模式是相當(dāng)靈活的。單個構(gòu)建器可以重復(fù)多次使?來構(gòu)建多個對象。構(gòu)建器的參數(shù)可以在build?法的調(diào)?之間進?調(diào)整以改變所創(chuàng)建的對象。構(gòu)建器可以在對象創(chuàng)建時?動填充?些字段,?如說每次創(chuàng)建?個對象時遞增的序列號等。

構(gòu)建器模式也有?身的缺點。為了創(chuàng)建對象,你必須要先創(chuàng)建其構(gòu)建器。雖然在實際情況中,創(chuàng)建構(gòu)建器的成本并不是很?,但在性能關(guān)鍵的情況下這就會導(dǎo)致問題了。此外,構(gòu)建器模式要?重疊的構(gòu)造?法模式更加冗?,這樣只有在參數(shù)數(shù)量?夠多的情況下使?構(gòu)建器模式才是值得的,?如說4個以上的參數(shù)。不過請記住,你可能會在未來增加更多的參數(shù)。但如果?開始使?的是構(gòu)造?法或是靜態(tài)??,當(dāng)參數(shù)數(shù)量變得很多時,想要切換到構(gòu)建器,那么顯?易?,會遺留很多廢棄的構(gòu)造?法或是靜態(tài)??。因此,更好的做法則是?開始就使?構(gòu)建器。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容