Effective Java Item2 建構函式參數過多可考慮使用建造器
整理Effective Java書中Item 2: Consider a builder when faced with many constructor parameters心得筆記
主旨
由於靜態工廠(static factories)和建構函式(constructors)還是有它的限制的,當參數變多了就不太適合,而進而衍伸出了這篇建造者模式(builder pattern)以及相關使用說明。
點出問題
- 典型的可伸縮建構函式(telescoping constructor)
public class Employee {
private String lastName;// required
private String firstName;// required
private String gender;// required
private Integer age;// optional
private Integer tel;// optional
private String address;// optional
private Boolean single;// optional
public Employee(String lastName, String firstName, String gender,
Integer age, Integer tel, String address, Boolean single) {
this.lastName = lastName;
this.firstName = firstName;
this.gender = gender;
this.age = age;
this.tel = tel;
this.address = address;
this.single = single;
}
}
Employee alice = new Employee("Chen", "Alice", "F", 25, null, null, 'Y');
當我們要使用它看起來會像這樣,而這類參數稍多的建構函式(constructor)有下面幾項問題:
無法快速了解,如果不點進去Employee class裡面查看,是不會知道傳進去的參數定義。
有序性,一定要按照建構函式裡定的順序設值。
不太優雅,有時候必須為了滿足參數傳了null。
沒有彈性,後來需求增加多了一個email,就在constructor裡面往後加,然後去找出所有使用到的地方去修改,明明很清楚這樣是硬幹(尤其要加第4個參數的時候…),但也懶得調整了心想先這樣吧,應該很多人都有類似經驗…
- JavaBeans pattern
另一種方式乾脆不要在constructor設值,用set的方式去設置所需要的
public class Employee {
private String lastName;// required
private String firstName;// required
private String gender;// required
private Integer age;// optional
private Integer tel;// optional
private String address;// optional
private Boolean single;// optional
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setGender(String gender) {
this.gender = gender;
}
public void setAge(Integer age) {
this.age = age;
}
public void setTel(Integer tel) {
this.tel = tel;
}
public void setAddress(String address) {
this.address = address;
}
public void setSingle(Boolean single) {
this.single = single;
}
}
當我們要使用它看起來會像這樣
Employee alice = new Employee();
alice.setLastName("Chen");
alice.setFirstName("Alice");
alice.setGender("F");
alice.setAge(25);
這總方式看起來可讀性好多了,但還是有不足之處:
- 太自由了,例如有個開發者少設置了一個必要欄位,如果又沒有嚴謹的驗證測試,上線可能會導致runtime exception。
- inconsistent state問題,多執行緒的環境下同時操作同一個物件,執行緒B把值改掉了導致執行緒A處理錯誤,也因為多執行緒環境無規律不好重現問題,有時候你需要使它成為不可變物件 (Immutable object)才行。
- 建造者模式(builder pattern)
進入本篇主題建造者模式(builder pattern)這個就是專門來優雅的解決上述問題的方法
public class Employee {
private String lastName;
private String firstName;
private String gender;
private Integer age;
private Integer tel;
private String address;
private Boolean single;
public static class Builder {
private String lastName;// required
private String firstName;// required
private String gender;// required
private Integer age;// optional
private Integer tel;// optional
private String address;// optional
private Boolean single;// optional
public Builder(String lastName, String firstName, String gender) {
this.lastName = lastName;
this.firstName = firstName;
this.gender = gender;
}
public Builder setAge(Integer age) {
this.age = age;
return this;
}
public Builder setTel(Integer tel) {
this.tel = tel;
return this;
}
public Builder setAddress(String address) {
this.address = address;
return this;
}
public Builder setSingle(Boolean single) {
this.single = single;
return this;
}
public Employee build() {
return new Employee(this);
}
}
private Employee(Builder builder) {
this.lastName = builder.lastName;
this.firstName = builder.firstName;
this.gender = builder.gender;
this.age = builder.age;
this.tel = builder.tel;
this.address = builder.address;
this.single = builder.single;
}
}
Employee alice = new Employee.Builder("Chen", "Alice", "F").setAge(25).setSingle(true);
其中要注意的是Employee建構函式(constructor)是private
的,而將必填欄位放在Builder的constructor裡面。說明只能透過Builder來創建,整體看起來更加優雅、容易閱讀、可擴展、無序性、不可變的、具安全性。
小結
本章節就是在介紹建造者模式(builder pattern),其實很多人已經有使用過,可以參考jdk裡的StringBuilder原始碼,這個是design pattern入門款之一,多嘗試看看使用這種方式。
補充小技巧
有人會問如果必填欄位很多怎麼使用Builder,不是也是要Builder的constructor放很多參數嗎?其實可以使用JDK7裡的
Objects.requireNonNull()
來擋控,至少能讓你在單元測試時期就發現bug。
private Employee(Builder builder) {
this.lastName = Objects.requireNonNull(builder.lastName);
this.firstName = Objects.requireNonNull(builder.firstName);
this.gender = Objects.requireNonNull(builder.gender);
this.age = Objects.requireNonNull(builder.age);
this.tel = Objects.requireNonNull(builder.tel);
this.address = builder.address;
this.single = builder.single;
}