Effective.Java 讀書筆記(2)使用Builder

2.Consider a builder when faced with many constructor parameters
大意爲當你面對大量的構造參數時考慮使用Builder

靜態工廠和構造器都有一個限制,它們不能夠很好地縮減大量地選項參數,想象一下一種情況,你的類有着很多的成員變量,有些必須填寫有些可以選填,那麼如果使用傳統的構造方法的話,排列組合一下可以想象會有多少個構造方法出現,這樣的情況不是我們所需要的,程序員們通常會使用一種名爲”伸縮構造函數模式(Telescopingconstructor  pattern)“的辦法,就是先提供必須要的選項參數作爲最簡單的構造方法,然後把非必須的選項參數逐漸加上去構成新的構造方法,不考慮組合的問題

舉個例子,現在你的構造方法有2個必須的參數A和B,然後有三個選填的參數C,D和E,那麼如果我們使用Telescoping constructor pattern,那麼代碼看上去還是比較簡潔的,只有4個不同的構造函數,如下

constructor(A a,B b){ //.....   }

constructor(A a,B b,C c){ //.....   }

constructor(A a,B b,C c,D d){ //.....   }

constructor(A a,B b,C c,D d,E e){ //.....   }

這樣出現的問題很明顯,當你想使用A,B和E作爲參數的時候,你不得不填上其他的所有參數,也就是C和D你也必須填上去

好吧,可能多填兩個參數你還是可以接受的話,不妨想象一下我們現在有着上百個參數,那麼麻煩就大了

簡要的總結一下,伸縮構造函數的模式的確起作用了,但是這對於代碼編寫和閱讀仍然有着一定的困難

這樣情況下,我們在使用的時候可能會因爲參數列表過長,然後不小心相互位置放錯而導致程序炸了,這在編譯階段可能看不出來錯誤

在你面對這樣許多參數的情況,有一種方法叫做JavaBeans的模式,這個模式很簡單,就是你只需要構造一個含所必需參數的構造函數,其他的選項都使用setter來設置即可,當然你的參數都是private的

這樣的模式消滅了伸縮構造帶來的煩惱,很簡單去實現,而且易於閱讀

但是這樣的模式也存在着或多或少的問題,因爲構造會在多次反覆調用中分裂,一個JavaBean 可能在他的構造中是不一致的狀態,什麼意思呢,就是說你如果使用JavaBean 那麼你所構造的類的參數是否完整並不是必須的,而且參數可能之前沒有,過一段代碼流程你又添加了,這就是不一致性,你所構造的類可能是缺少參數的,但是我們在調用一些方法的時候並不會去檢查這些參數的存在性,那麼就可能導致問題的出現,debug起來可能也較爲困難

還有一個JavaBean模式的問題就是,這一種模式排除了使一個類變成不變的類(Immutable Class)的可能性,而且需要在程序員保重線程安全的部分做出額外的努力

這些缺點呢,我們可以當構造結束時手動地”冰凍“(freezing)這些對象並且不允許被它使用直到它被解凍來減少這些缺點,當然這個方法也有許多問題存在,比如編譯器並不能確定你所使用地方法是否被凍結了。

幸運的是,這裏有第三種解決方案,既包括了伸縮構造模式的安全性又有JavaBeans模式的可讀性。它就是Builder模式,並非直接地創建一個需要的對象,用戶先調用一個需要全部必需參數的構造方法,然後得到一個builder對象,接着用戶使用類似setter的方法來在builder上設置參數,最後調用build方法來生成對象,這樣生成的對象是immutable(不可變的),builder在它所build的類中是一個靜態的成員類

這裏給出書中的例子


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 carbohydrate = 0;
private int sodium = 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 carbohydrate(int val)
          { carbohydrate = val; return this; }
public Builder sodium(int val)
          { sodium = 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;
}
}

從例子中我們可以看出,這一模式就是利用Builder類來初始化參數,設置參數,然後再把自己作爲參數傳入主類的構造函數中,並且給參數賦值實現對象的建立,注意其中的類似於setter的設置,返回的是this,所以可以使用鏈式的調用,比如

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

這樣的程序語句易於我們去編寫,更重要的是,容易讀,Builer模式模擬命名了選項參數可以在Python或者Ada中看到它的蹤影

就像一個構造方法一樣,一個Builder能夠強加不變的性質在它的參數上,build方法可以檢查這些不變量,重要的是,它們在複製參數從builder到對象之後會被檢查,是在對象的域進行檢查。如果有不變量是衝突的,build會拋出一個名爲 IllegalStateException的異常,這個方法會提示哪一個不變量是衝突的

在多個參數中強加不變量的另一個方法是使用setter方法設置整個參數組,如果不變量不滿足要求,那麼setter方法就會拋出一個名爲IllegalArgumentException的異常

對於builder來說,一個次要的優點是builder可以擁有多個變量參數。而構造方法只能有一個變量參數,因爲builder使用分離的多個方法來設置相應的參數(解釋一下,構造方法,或者說方法的變量參數只能是一個 比如  A(int a){},就不能是A(int a1 a2 a3){}這樣)

Builder模式十分靈活,一個builder可以被用來build多個對象,builder的參數可以被調整使得對象不同,builder可以自動的補充某個域,在對象生成的時候會自動產生一系列的數字

一個所擁有參數被事先設置的builder構成一個良好的抽象工廠(Abstract Factory),換句話說,我們可以通過這樣一個builder來變成一個方法去創建多個對象,爲了實現這樣的使用方案,我們需要一個類型來表達builder

// A builder for objects of type T
public interface Builder<T> {
     public T build();
}

擁有這一個Builder實例地方法會特別地約束builder的類型參數,這個類型參數使用有界通配符,舉個例子,這裏有個使用用戶提供的Builder實例來創建相對應節點來創建一棵樹的方法

Tree buildTree(Builder<? extends Node> nodeBuilder) { ... }

傳統的抽象工廠在Java上的實現曾經是一個類的對象,有着newInstance方法,這個方法起到了build方法的作用。這樣的用法有着問題,這個newInstance方法呢經常企圖調用類的無參構造方法,但這個無參的構造方法可能並不存在,當這個類沒有可用的無參構造方法的時候你不會在編譯階段得到一個error,那麼應對這個問題我們使捕獲InstantiationException或者IllegalAccessException來解決,但是這樣太醜了而且不方便。Class.newInstance 破壞了編譯階段exception的檢查,使用Builder接口就可以解決這些缺陷

當然Builder模式也是有缺點的,創建一個類的時候你必須先創建builder,你必須確定一下創建一個builder的代價開銷,在某些情況下可能是個重要的問題。當然builder對於伸縮構造模式來說更爲詳細,它只創建你需要的參數下的對象,當然參數足夠多建議使用builder,否則可能沒有什麼意義,如果你的參數有4個或者更多而且後期可能繼續添加,請第一時間想到使用builder模式作爲類編寫的開始。

總結,Builder模式當我們設計一個有着許多需要處理的參數的類的時候是一個好的選擇,特別是其中的許多參數都是可選的,我們的代碼使用builders比使用傳統伸縮構造模式更加易於讀和寫,比起JavaBeans更加安全。
發佈了45 篇原創文章 · 獲贊 12 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章