構造模式實踐

jopen 9年前發布 | 8K 次閱讀 模式

這是我第一篇文章(也是我關于這個主題的第一篇博客)。我記不清在哪讀過這項內容(盡管我基本上確認是在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,有一些插件可以讓你避免建造者中大部分的官樣文章代碼。下面這三個是比較推薦的:

我個人還沒使用過其中任何一種插件,所以對于哪個更好,我沒辦法提供一個指導性的意見。我估計其他IDE應該也有類似的插件。

原文鏈接: jlordiales 翻譯: ImportNew.com - 孟 冰川
譯文鏈接: http://www.importnew.com/14849.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!