《Effective Java》学习笔记2 Consider a builder when faced with many constructor parameters

版权声明:欢迎转载,但麻烦注明出处 https://blog.csdn.net/q2878948/article/details/81059800

本栏是博主根据如题教材进行Java进阶时所记的笔记,包括对原著的概括、理解,教材代码的报错&运行情况。十分建议看过原著遇到费解地方再来参考或与博主讨论。致敬作者Joshua Bloch跟以杨春花为首的译者团队。

  遇到多个构造器参数时要考虑用构建器 

一般当我们需要多个可选参数时,会这样做:

public class BadNutritionFacts {
    private int servingSize;    //ml
    private int servings;       //per container
    private int calories;       //
    private int fat;            //g
    private int sodium;         //mg
    private int carbohydrate;   //mg


    public BadNutritionFacts(int servingSize, int servings) {
        this(servingSize , servings  ,0);
    }

    public BadNutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize , servings , calories ,0);
    }

    public BadNutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize , servings , calories , fat , 0);
    }

    public BadNutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize , servings , calories , fat , sodium , 0);
    }

    public BadNutritionFacts(int servingSize, int servings, int calories,
                             int fat, int sodium, int carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }

但这样的话随着参数增加,容易失控,可读性也奇差,而且就算我们不小心颠倒了其中两个参数的顺序,也不会报错。这时推荐使用JavaBeans模式:

public class NutritionFacts {
    private int servingSize = -1;   //ml
    private int servings = -1;      //per container
    private int calories = 0;       //
    private int fat = 0;            //g
    private int sodium = 0;         //mg
    private int carbohydrate = 0;   //mg

    public NutritionFacts() {
        super();
    }

    public void setServingSize(int servingSize) {
        this.servingSize = servingSize;
    }

    public void setServings(int servings) {
        this.servings = servings;
    }

    public void setCalories(int calories) {
        this.calories = calories;
    }

    public void setFat(int fat) {
        this.fat = fat;
    }

    public void setSodium(int sodium) {
        this.sodium = sodium;
    }

    public void setCarbohydrate(int carbohydrate) {
        this.carbohydrate = carbohydrate;
    }
}

为了解决{@link BadNutritionFacts}中的问题,在使用时我们调用一个无参构造器创建对象,然后用setter方法设置各个必要参数、可选参数。

    public static void main(String[] args) {
        NutritionFacts nutritionFacts = new NutritionFacts();
        nutritionFacts.setServings(10);
        nutritionFacts.setServingSize(20);
        nutritionFacts.setCalories(50);
        nutritionFacts.setSodium(90);
        //use nutritionFacts do sth 
    }

但是这种方法仍然存在问题:调用setter方法过程中存在状态不一致问题,需要额外的线程安全控制,而且这种情况调试起来非常困难。于是,我们现在祭出更加优秀的一种设计方法——兼顾线程安全和可读性的Builder模式:

Builder模式编写出来一个公共的静态内部类Builder,在静态内部类中提供链式调用的方法(return this和build()),Builder设构造方法以传入不可缺省的必要参数,私有化BestNutrition的构造方法,为使用者提供Builder的构造方法来构造对象:

public class BestNutritionFacts {
    //Required parameters
    private int servingSize;    //ml
    private int servings;       //per container

    //Optional parameters
    private int calories;       //
    private int fat;            //g
    private int sodium;         //mg
    private int carbohydrate;   //mg

    public void nutritionFactsInfo(){
        System.out.println("servingSize , servings , calories , fat , sodium , carbohydrate");
        System.out.println(servingSize + " , " + servings + " , " + calories + " , " +fat + " , " + sodium + " , " +carbohydrate);
    }

    private BestNutritionFacts(Builder builder){
        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories = builder.calories;
        this.fat = builder.fat;
        this.sodium = builder.sodium;
        this.carbohydrate = builder.carbohydrate;
    }

    public static class Builder{
        //Required parameters
        private int servingSize;    //ml
        private int servings;       //per container

        //Optional parameters
        private int calories = 0;       //
        private int fat = 0;            //g
        private int sodium = 0;         //mg
        private int carbohydrate = 0;   //mg

        public Builder(int servingSize , int servings){
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val){
            this.calories = val;
            return this;
        }
        public Builder fat(int val) {
            this.fat = val;
            return this;
        }
        public Builder sodium(int val) {
            this.sodium = val;
            return this;
        }

        public Builder carbonhydrate(int val) {
            this.carbohydrate = val;
            return this;
        }

        public BestNutritionFacts build(){
            return new BestNutritionFacts(this);
        }
    }
}

另外,builder可以在builder方法中对参数强加约束条件,并在对象域而不是builder域中对它们进行检验,或者也可以直接在setter中进行检验,这样就不必等到builder才发现错误,比如修改后的Builder.build():

        public BestNutritionFacts build(){
            if (this.servings < 0 || this.servingSize < 0){
                System.out.println("illegal args!");
                throw new IllegalArgumentException();
            }
            return new BestNutritionFacts(this);
        }

 builder十分灵活,单个builder可以创建多个对象,其参数也可以在创建期间进行调整,也可以实现比如主键自增等操作。另外,builder还可以通过参数生成抽象工厂:

/**
 * builder的抽象工厂,通过泛型控制返回类型,
 * 而在实现该接口的类中加以限制如Tree buildTree(Builder<? extends Node> nodeBuilder);进行约束。
 *
 * 传统的抽象工厂是用Class对象的newInstance充当build的一部分,但newInstance总是调用其无参构造方法,
 * 而不管这种构造方法存不存在,于是客户端就需要处理InstantiationException或者IllegalAccessException;
 * 加上newInstance会传递无参构造方法所抛出的异常,即使newInstance缺乏相应的throws子句。
 * 也就是说,newInstance破坏了Java的异常检查,
 * 而使用Builder的异常检测机制则可以解决这个问题{@link nutrition.BestNutritionFacts}
 *
 * @author LightDance
 */
public interface Builder<T> {

    public T build();
    //然后创建Builder类时implements一下就可以了
}

最后是Builder模式的短板:

builder的主要缺陷是为了创建对象不得不先编写一个Builder对象,这或许会在对于性能要求十分严格的情况下造成一些问题。而且有些时候Builder方法也会非常冗长,因此只在参数很多或者感觉将来版本中参数会逐渐增多的时候使用Builder. 

但是,从构造方法或者静态工厂改到Builder时,会出现很多过时的构造方法和静态工厂方法,增加更改代码逻辑的困难,因此最好一开始就使用最合适的那种方法创建对象。

简而言之,在准备写代码之前要对需求有客观可靠的评估,然后根据评估结果选择敲何种风格的代码。

全代码github地址:点我点我

猜你喜欢

转载自blog.csdn.net/q2878948/article/details/81059800