當前位置:
首頁 > 知識 > 構造模式實踐

構造模式實踐

(點擊

上方公眾號

,可快速關注)




來源: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技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 ImportNew 的精彩文章:

通向架構師的道路(第十八天)萬能框架 Spring ( 一 )(下)
使用 partclone 備份磁碟分區

TAG:ImportNew |