Java Builder模式

当构造函数只有一两个参数的时候,一切都很顺利,但超过三个参数以后还使用构造函数来初始化就有些弊端,尤其是参数类型相似的时候,比如下面这样

public NutritionFacts(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;
}

你需要仔细的核对传入的参数,,即使IDE有强大的提示功能,有时不小心将传入的参数顺序搞乱,它在编译的时候不会出错,而在运行时的反常让人摸不着头脑。也许你会尝试使用下面这个JavaBeans Pattern解决问题

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

这种分步式的调用可能导致对象的构造只完成了部分,很明显这样的对象是危险的,而且能使用set方法赋值,这个类就不能是不可变的,在多线程安全方面又处于劣势。我们推荐的方式是Builder 模式。

//不可变类,成员变量都是final
public class NutritionFacts {
    private final int servings;
    private final int servingSize;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
    //私有的构造方法,使用build来完成初始化
    private NutritionFacts(Builder builder) {
        servings = builder.servings;
        servingSize = builder.servingSize;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
    //功能上看就是一个参数收集器,final修饰必选参数,其他为可选参数,但是要有默认值
    public static class Builder {
        private final int servings;
        private final int servingSize;
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

        public Builder(int servings, int servingSize) {
            this.servings = servings;
            this.servingSize = servingSize;
        }
        //良好的函数名,调用时识别度高,不容易出错
        public Builder calories(int val) {
            calories = val;
            return this;
        }
        public Builder fat(int val) {
            fat = val;
            return this;
        }
        public Builder sodium(int val) {
            sodium = val;
            return this;
        }
        public Builder carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
}

NutritionFacts对象的初始化既简单又安全还方便,Builder构造方法和可选的链式调用完成参数的收集,最后集中在build方法中完成NutritionFacts的初始化

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                              .calories(100).sodium(35).carbohydrate(27).build();

尽量在Builder的构造函数和赋值方法中检测参数的有效性,同时,在使用Builder中的参数初始化对象字段时最好使用防御性赋值,然后再检查一番,上面的都是基本变量,看不出来。什么是防御性复制,如下示例:

  Date start = new Date();
  Date end = new Date();
  Period p = new Period(start, end);
  //恶意攻击
  end.setYear(78);

没有使用防御性复制,即使开始检查通过,外部仍持用引用 破坏不可变性 埋下隐患
public Period(Date start, Date end) {
    if (start.compareTo(end) > 0)
      throw new IllegalArgumentException(start + " after " + end);
      this.start = start;
      this.end = end;
}
使用防御性复制,和外部断开联系 保证不变性
public Period(Date start, Date end) {
      this.start = new Date(start.getTime());
      this.end = new Date(end.getTime());
      if (this.start.compareTo(this.end) > 0) 
            throw new IllegalArgumentException(this.start + " after " + this.end);
}

下面是抽象泛型Builder,可以很好的整合在类的结构中

public abstract class Pizza {
    浇头的枚举值,有火腿、蘑菇、葱、胡椒、香肠
    enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
    private final EnumSet toppings;
    //泛型参数必须是此Builder的子类,递归类型参数
    static abstract class Builder> {
        //使用静态工厂方法根据参数可以返回不同的子类
        // EnumSet.noneOf在枚举数量小于等于64时返回的是RegularEnumSet,
        //正好用一个64位的long类型通过位运算来标记添加的元素
        private EnumSet toppings = EnumSet.noneOf(Topping.class);
        public T add(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }
        //返回子类,这样子类不用cast也能链式调用
        protected abstract T self();
        abstract Pizza build();
    }
    public Pizza(Builder builder) {
        //防御性复制,防止外部持有引用破坏不变性
        this.toppings = builder.toppings.clone();
    }
}

子类在结构上和父类一致

public class NyPizza extends Pizza {
    public enum Size {SMALL, MEDIUM, LARGE}
    private final Size size;
    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
    //不要忘了Pissa.Builder的泛型为此Builder,否则self方法返回的是父类引用,
    public static class Builder extends Pizza.Builder {
        private final Size size;
        public Builder(Size size) {
            this.size = size;
        }
        @Override
         //此Builder为返回类型的子类,协变返回类型
        protected Builder self() {
            return this;
        }
        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }
    }
}
public class Calzone extends Pizza {
    private final boolean sauceInside;

    private Calzone(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }

    public static class Builder extends Pizza.Builder {
        private boolean sauceInside = false;
     
        @Override
        protected Builder self() {
            return this;
        }
        public Builder sauceInside() {
            this.sauceInside = true;
            return this;
        }
        @Override
        public Calzone build() {
            return new Calzone(this);
        }
    }
}

对象的构造兼具灵活和安全,可读性强不容易出错

 NyPizza pizza = new NyPizza.Builder(NyPizza.Size.LARGE).add(Pizza.Topping.HAM).build();
 Calzone calzone=new Calzone.Builder().sauceInside().add(Pizza.Topping.MUSHROOM).build();

这个方法的缺点在于每次创建对象都需要一个额外的Builder对象来收集参数,如果对这一点不敏感,使用Builder来创建对象是一个不错的选择,而且有些库如Lombok可以使用注解的方式帮你生成Builder类避免了繁杂的手动赋值。

你可能感兴趣的:(Java Builder模式)