構造模式實踐
(點擊
上方公眾號
,可快速關注)
來源:ImportNew - 孟冰川
這是我第一篇文章(也是我關於這個主題的第一篇博客)。我記不清在哪讀過這項內容(儘管我基本上確認是在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應該也有類似的插件。
看完本文有收穫?請轉發分享給更多人
關注「ImportNew」,提升Java技能
※通向架構師的道路(第十八天)萬能框架 Spring ( 一 )(下)
※使用 partclone 備份磁碟分區
TAG:ImportNew |