歡迎光臨
每天分享高質量文章

構造樣式實踐

(點選上方公眾號,可快速關註)


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

贊(0)

分享創造快樂