整理Effective Java書中Item 2: Consider a builder when faced with many constructor parameters心得筆記

主旨

由於靜態工廠(static factories)和建構函式(constructors)還是有它的限制的,當參數變多了就不太適合,而進而衍伸出了這篇建造者模式(builder pattern)以及相關使用說明。

點出問題

  1. 典型的可伸縮建構函式(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個參數的時候…),但也懶得調整了心想先這樣吧,應該很多人都有類似經驗…

  1. 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)才行。
  1. 建造者模式(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;
}