java設計模式——建造者模式(對建構函式的優化)
我不打算跳入設計模式的過多細節中,因為已經有一大堆的文章和書籍很詳細的解釋過了。所以我打算告訴你為什麼和什麼時候你應該考慮使用設計模式。然而,值得一提的是本文中的模式和四人幫的《Design Patterns: Elements of Reusable Object-Oriented Software》一書中的提出的有點不一樣。因為原生模式專注於抽象構建的步驟,所以通過使用不同的建造者的實現我們能得到不同的結果,然而在本文中解釋的設計模式是討論如何移除源自於多建構函式,多可選引數和濫用的setters方法中那些不必要的複雜的事物。
假設你有一個包含大量屬性的類,就像下面的User類一樣。讓我們假設你想讓這個類不可變(順便說一句,除非你有一個真正的好理由,讓你不必總是向著不可變這個目標奮鬥。我們會在另一篇文章中討論它)。
public class User {
private final String firstName; //required
private final String lastName; //required
private final int age; //optional
private final String phone; //optional
private final String address; //optional
...
}
現在想象一下,在你的類中有一些屬性是必須的,有一些是可選的。你會如何建立這個類的例項?所有的屬性都被宣告成final型別,所以你必須在構造方法中設定它們,但是你也想讓這個類的客戶端有忽略可選屬性的機會。
一個首先想到的可選方案是有一個構造方法是隻接收必須屬性作為引數,一個是接收所有必須屬性和第一個可選屬性,再一個是接收兩個可選屬性等等。這看起來會是什麼樣的?
看起來這樣:
public User(String firstName, String lastName) {
this(firstName, lastName, 0);
}
public User(String firstName, String lastName, int age) {
this(firstName, lastName, age, '');
}
public User(String firstName, String lastName, int age, String phone) {
this(firstName, lastName, age, phone, '');
}
public User(String firstName, String lastName, int age, String phone, String address) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.phone = phone;
this.address = address;
}
這種構造物件的方式的好處是它可以正常工作。然而,這種方式的問題也是顯而易見的。當你只有幾個屬性的時候不是什麼大問題,但是隨著屬性個數的增加,程式碼變的越來越難閱讀和維護。更重要的是,對客戶端來說程式碼變得越來越難使用。
客戶端裡我該呼叫那一個構造方法?有兩個引數那個?三個引數那個?我沒有顯示傳值的那些引數的預設值都是什麼?如何我只想給address屬性設定一個值,但是不想給age和phone設定該怎麼辦?這種情況下,我不得不呼叫能接收所有引數的構造方法並傳遞預設值給那些我不關心的可選引數。另外,一些引數有相同的型別很容易混淆。第一個String型別引數對應的是phone還是address?
因此我們有什麼其他選擇來應對這種場景?我們可以總是遵循JavaBeans慣例,有一個預設的無參構造方法並且每個屬性都有getter和setter方法。就象這樣:
public class User {
private String firstName; // required
private String lastName; // required
private int age; // optional
private String phone; // optional
private String address; //optional
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
這種方法看起來很容易閱讀和維護。在客戶端裡我可以只建立一個空物件,然後只設置那些我感興趣的屬性。那麼,這種方法有什麼問題?這種解決方案有兩個主要的問題。第一個問題是該類的例項狀態不固定。如果你想建立一個User物件,該物件的5個屬性都要賦值,那麼直到所有的setXX方法都被呼叫之前,該物件都沒有一個完整的狀態。這意味著在該物件狀態還不完整的時候,一部分客戶端程式可能看見這個物件並且以為該物件已經構造完成。這種方法的第二個不足是User類是易變的。你將會失去不可變物件帶來的所有優點。
幸運的是應對這種場景我們有第三種選擇,建造者模式。解決方案類似如下所示:
public class User {
private final String firstName; // required
private final String lastName; // required
private final int age; // optional
private final String phone; // optional
private final String address; // optional
private User(UserBuilder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
public String getPhone() {
return phone;
}
public String getAddress() {
return address;
}
public static class UserBuilder {
private final String firstName;
private final String lastName;
private int age;
private String phone;
private String address;
public UserBuilder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
public UserBuilder phone(String phone) {
this.phone = phone;
return this;
}
public UserBuilder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
一些值得注意的關鍵點:
User構造方法是私有的,這意味著該類不能在客戶端程式碼裡直接例項化。
該類現在又是不可變的了。所有屬性都是final型別的,在構造方法裡面被賦值。另外,我們只為它們提供了getter方法。
builder類使用流式介面風格,讓客戶端程式碼閱讀起來更容易(我們馬上就會看到一個它的例子)。
builder類構造方法只接收必須屬性,為了確保這些屬性在構造方法裡賦值,只有這些屬性被定義成final型別。
使用建造者模式有在本文開始時提到的兩種方法的所有優點,並且沒有它們的缺點。客戶端程式碼寫起來更簡單,更重要的是,更易讀。我聽過的關於該模式的唯一批判是你必須在builder類裡面複製類的屬性。然而,考慮到這個事實,builder類通常是需要建造的類的一個靜態類成員,它們一起擴充套件起來相當容易。
現在,試圖建立一個新的User物件的客戶端程式碼看起來如何那?讓我們來看一下:
public User getUser() {
return new
User.UserBuilder('Jhon', 'Doe')
.age(30)
.phone('1234567')
.address('Fake address 1234')
.build();
}
非常整潔,是不是?你可以只用一行程式碼就建立一個User物件,更重要的是,程式碼很易讀。此外,我們也確保無論何時你獲得一個該類的物件,該物件都不會是一個不完整的狀態。
這個模式是非常靈活的。一個單獨的builder類可以通過在呼叫build方法之前改變builder的屬性來建立多個物件。builder類甚至可以在每次呼叫之間自動補全一些生成的欄位,例如一個id或者序列號。
很重要的一點是,例如構造方法,builder類可以在構造方法引數上增加約束。build方法可以檢查這些約束,如果不滿足就丟擲一個IllegalStateException異常。
至關重要的是要在builder的引數拷貝到建造物件之後在驗證引數,這樣驗證的就是建造物件的欄位,而不是builder的欄位。這麼做的原因是builder類不是執行緒安全的,如果我們在建立真正的物件之前驗證引數,引數值可能被另一個執行緒在引數驗證完和引數被拷貝完成之間的時間修改。這段時間週期被稱作“脆弱之窗”。
在我們User的例子中,類似程式碼如下:
public User build() {
User user = new user(this);
if (user.getAge() 120) {
throw new IllegalStateException(“Age out of range”); // thread-safe
}
return user;
}
上一個程式碼版本是執行緒安全的因為我們首先建立user物件,然後在不可變物件上驗證條件約束。下面的程式碼在功能上看起來一樣但是它不是執行緒安全的,你應該避免這麼做:
public User build() {
if (age 120) {
throw new IllegalStateException(“Age out of range”); // bad, not thread-safe
}
// This is the window of opportunity for a second thread to modify the value of age
return new User(this);
}
建造者模式最後的一個優點是builder可以作為引數傳遞給一個方法,讓該方法有為客戶端建立一個或者多個物件的能力,而不需要知道建立物件的任何細節。為了這麼做你可能通常需要一個如下所示的簡單介面:
public interface Builder {
T build();
}
借用之前的User例子,UserBuilder類可以實現Builder。如此,我們可以有如下的程式碼:
UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}
好吧,這確實是一篇很長的文章,雖然是第一次發。總而言之,建造者模式在多於幾個引數(雖然不是很科學準確,但是我通常把四個引數作為使用建造者模式的一個很好的指示器),特別是當大部分引數都是可選的時候。你可以讓客戶端程式碼在閱讀,寫和維護方面更容易。另外,你的類可以保持不可變特性,讓你的程式碼更安全。