2020/06/22

[筆記] Effective Java #2 遇到多個 constructor 參數時要考慮使用 builder

Effective Java 3rd 簡體中文版筆記 #2 遇到多個 constructor 參數時要考慮使用 builder
無論是 static factory method 或是 constructor,在遇到傳入的參數很多時,都不容易處理。在開發過程中,常會使用 Telescoping Constructor Pattern 來解決。
public class PricingModel {
    private final String name;
    private final double smsFee;
    private final int smsFreeTimes;
    private final double phoneFee;
    private final int phoneFreeMinutes;
    private final double networkFee;
    private final int networkFreeGB;

    public PricingModel(String name, double smsFee, int smsFreeTimes) {
        this(name, smsFee, smsFreeTimes, 0, 0, 0, 0);
    }

    public PricingModel(String name, double smsFee, int smsFreeTimes, 
                        double phoneFee, int phoneFreeMinutes) {
        this(name, smsFee, smsFreeTimes, phoneFee, phoneFreeMinutes, 0, 0);
    }
    
    public PricingModel(String name, double smsFee, int smsFreeTimes, 
                        double phoneFee, int phoneFreeMinutes, 
                        double networkFee, int networkFreeGB) {
        this.name = name;
        this.smsFee = smsFee;
        this.smsFreeTimes = smsFreeTimes;
        this.phoneFee = phoneFee;
        this.phoneFreeMinutes = phoneFreeMinutes;
        this.networkFee = networkFee;
        this.networkFreeGB = networkFreeGB;
    }
}
當需要傳入的參數越來越多時,上述的 Telescoping constructor pattern 同樣會讓開發者誤用及不易閱讀,那如果換成單純的 JavaBeans Pattern,開發者在使用上較簡單,程式碼也較容易閱讀。
public class PricingModel {
    private String name;
    private double smsFee;
    private int smsFreeOfTimes;
    private double callFee;
    private int callFreeOfMinute;
    private double networkFee;
    private int networkFreeOfGB;

    public void setName(String name) { this.name = name; }
    public void setSmsFee(double smsFee) { this.smsFee = smsFee; }
    public void setSmsFreeOfTimes(int smsFreeOfTimes) { this.smsFreeOfTimes = smsFreeOfTimes; }
    public void setCallFee(double callFee) { this.callFee = callFee; }
    public void setCallFreeOfMinute(int callFreeOfMinute) { this.callFreeOfMinute = callFreeOfMinute; }
    public void setNetworkFee(double networkFee) { this.networkFee = networkFee; }
    public void setNetworkFreeOfGB(int networkFreeOfGB) { this.networkFreeOfGB = networkFreeOfGB; }
}
但是 setter 會讓 JavaBean 的不可變性打開後門,當 JavaBean 生成時,開發者有機會去修改其內容。範例如下,在拿到 PricingModel 後,利用 setter 修改 SMS 的免費次數。
public class PricingModel {
    private static final PricingModel GENERAL = ...;
    public static PricingModel general() {
        return GENERAL;
    }
}

// use case
PricingModel general = PricingModel.general();
general.setSmsFreeOfTimes(Integer.MAX_VALUE);
最後介紹一種替代方案,可以避免掉上述的問題,Builder Pattern。它是由另一個 builder class 來處理,builder 中的 setter 回傳的是 builder 本身,最後調用 build() 才會生成實體。
class PricingModelBuilder {
    private String name;
    private double smsFee;
    private int smsFreeOfTimes;
    private double callFee;
    private int callFreeOfMinute;
    private double networkFee;
    private int networkFreeOfGB;

    public PricingModelBuilder setName(String name) {
        this.name = name;
        return this;
    }

    public PricingModelBuilder setSmsFee(double smsFee) {
        this.smsFee = smsFee;
        return this;
    }

    public PricingModelBuilder setSmsFreeOfTimes(int smsFreeOfTimes) {
        this.smsFreeOfTimes = smsFreeOfTimes;
        return this;
    }
    
    // other setter...
    
    public PricingModel build() {
        return new PricingModel(name, smsFee, smsFreeOfTimes, 
        callFee, callFreeOfMinute, networkFee, networkFreeOfGB);
    }
}

// use case
PricingModel general = new PricingModelBuilder().setName("general")
            .setSmsFee(2.4).setSmsFreeOfTimes(5).build();
Builder Pattern 的另一項好處是,它可以多次調用某一方法,假設 PricingModel 有一 List,如果使用 setter,會是開發者準備好 List 後,由 setter 傳入,但在 builder 中可以有延伸,像是傳入參數的檢查,或是提供更簡潔的新增 element 方式。
// constructor setter
public void setPromoteProjects(List<String> promoteProjects) {
    this.promoteProjects = promoteProjects;
}
// builder method
public PricingModelBuilder addToPromoteProject(String project) {
    this.promoteProjects.add(project);
    return this;
}

// use case: constructor & builder
PricingModel general = new PricingModel();
List<String> projects = new ArrayList<>();
projects.add("12M");
projects.add("24M");
projects.add("30M");
general.setPromoteProjects(projects);

PricingModel general = new PricingModelBuilder()
    .addToPromoteProject("12M")
    .addToPromoteProject("24M")
    .addToPromoteProject("30M")
    .build();
轉載請註明原文網址 https://cookieandcoketw.blogspot.com/2020/06/effective-java-2-constructor-builder.html

沒有留言:

張貼留言