構造模式實踐
這是我第一篇文章(也是我關于這個主題的第一篇博客)。我記不清在哪讀過這項內容(盡管我基本上確認是在Practices of an Agile Developer上看到的),但是寫博客應該能幫助你全神貫注。具體點來說,通過花些時間來解釋你所知道的東西,你能更好的理解它。
這也正是我想要努力去做的,通過解釋一件事,繼而進一步理解這件事。并且還有個額外的好處,當我回憶曾經做過的事情時,它是一個很好的集中地。希望在這過程中也能幫助到你們。
廢話不多,讓我們直奔主題——構造模式。我不打算分割成許多細節來講,因為已經有非常多的稿件,書籍詳細的說明過這個模式。 反而,我會告訴你為什么,以及什么時候應該考慮使用它。然而,值得一提的是,這里所說的模式和四人幫書中的模式有些不同。盡管原始的模式聚焦于抽象出構建的步驟,這樣通過改變建造者的實現就可以得到不同的結果,本篇所說的模式著眼于從多構造器,多可選參數以及過度使用的setter中移除不必要的復雜性。
想象下你有一個類,像下圖所示有許多屬性。假設你想讓你的類不可變(順便說一下,除非有一個好的理由不這樣做,否則你應該堅持。但是我們會以另一種方式來達到要求。)
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; }
這種方式來構建類的實例的好處是它能很好的工作。然而,這種方式的問題也很明顯。當你只有幾個屬性時還好,但是當這個數字擴大時,代碼就變的難以理解和維護了。
更重要的是,代碼對客戶端來說變的很難。客戶端應該調用哪個構造器?有兩個參數的?有三個參數的?那些不用傳確切值的參數的默認值是多少?如果我想給地址賦值,但是不給age和phone賦值要怎么辦?那種情況下,我就不得不調用接受所有參數的構造器,并且給那些不需要的傳入不在乎的默認值。此外,幾個類型相同的參數是很令人費解的。第一個String是電話還是地址? 那么在這些情況下,我們還有其他選擇嗎?我們可以依照JavaBeans的約定,一個無參構造并且每個參數提供一個get和set。類似下面這個:
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; } }
這 種方式看起來容易理解和維護。作為客戶端,我只需要創建一個空對象并且set我感興趣的屬性即可。那么這種方式有什么弊端呢?有兩個主要弊端。第一個是類 實例的不一致狀態。如果你要用User的五個屬性來創建一個User對象,那么在所有的setX方法調用前,對象處于不完全狀態。這就意味著客戶端的其他 部分可能看到對象,并且假設它已經完成構造了,實際它并沒有。方法的第二個缺點是對象可變。你喪失了不可變對象的所有好處。
幸運的是還有第三個選擇,建造者模式,方案看起來是下面這樣的。
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操作。
- 建造者使用流式接口習語來讓客戶端代碼更易讀(下面會有示例)。
- 建造者的構造器只接受兩個必須的參數,并且這兩個屬性是僅有的被設置為final類型的,這樣就能保證這些屬性在構造器中是被賦值的。
建造者模式的使用擁有開始所提兩種方案的所有優點,并且沒有它們的缺點。客戶代碼更容易寫,最重要的是更易讀。關于這個模式,我聽到的唯一缺點是必須要復制類的屬性到建造者中。既然建造者類通常是它所建造類的一個靜態成員類,它們能相當容易的一起演進。
那么,客戶代碼嘗試創建一個新的User對象會是什么樣的?讓我們來看看:
public User getUser() { return new User.UserBuilder("Jhon", "Doe") .age(30) .phone("1234567") .address("Fake address 1234") .build(); }
很工整,不是嗎?你能在一行內創建一個User對象,最重要的是它很容易理解。而且,你能確保,無論什么時候你拿到這個類的一個對象,它的狀態都是完整的。
這個模式非常靈活。一個建造者可以通過在多次調用“build”之間改變屬性用來創建多個對象。構造者甚至可以在兩次調用之間自動補全一些生成的字段。例如id或其他序列號。
重點是,類似于構造器,建造者可以強制其參數的不變性。建造方法可以檢查這些不變性, 如果它們無效就拋出IllegalStateException異常。關鍵是可以在從建造者中拷貝參數到對象時檢查,并且是在對象字段上檢查而不是在構造 器字段。這樣做的理由是,既然建造者不是線程安全的,如果我們在實際創建對象前檢查參數,參數值可能會在檢查和拷貝之間被另一個線程改變。這個階段的時間 被認為是“易損窗口”。在我們的例子中看起來是如下這樣的:
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); }
最后一個優點是建造者可以被傳入到一個方法中,來讓這個方法為客戶創建一個或多個對象,而不用知道任何對象創建的細節。你通常需要一個簡單的接口來完成此功能:
public interface Builder { T build(); }
在上面的例子中,UserBuilder類可以實現Builder接口。我們就可以使用下面這種方式:
UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}
這真是個很長的首發文章。總結一下,建造者模式是處理超過一個參數的類的絕佳選擇(這不是嚴格意義上的說法,但是我通常將接受四個屬性的類當成使用這種模式的暗示),特別是如果大部分的參數是可選的。你的客戶端代碼會更易讀,易寫,易維護。此外,你的類可以保持不變,這點可以讓你的代碼更安全。
更新:如果你使用Eclipse作為你的IDE,有一些插件可以讓你避免建造者中大部分的官樣文章代碼。下面這三個是比較推薦的:
- http://code.google.com/p/bpep/
- http://code.google.com/a/eclipselabs.org/p/bob-the-builder/
- http://code.google.com/p/fluent-builders-generator-eclipse-plugin/
我個人還沒使用過其中任何一種插件,所以對于哪個更好,我沒辦法提供一個指導性的意見。我估計其他IDE應該也有類似的插件。
原文鏈接: jlordiales 翻譯: ImportNew.com - 孟 冰川
譯文鏈接: http://www.importnew.com/14849.html