美文网首页
Java Builder模式

Java Builder模式

作者: 风骚无俩 | 来源:发表于2020-02-20 12:02 被阅读0次

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

    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<Topping> toppings;
        //泛型参数必须是此Builder的子类,递归类型参数
        static abstract class Builder<T extends Builder<T>> {
            //使用静态工厂方法根据参数可以返回不同的子类
            // EnumSet.noneOf在枚举数量小于等于64时返回的是RegularEnumSet,
            //正好用一个64位的long类型通过位运算来标记添加的元素
            private EnumSet<Topping> 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<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<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模式

          本文链接:https://www.haomeiwen.com/subject/idetqhtx.html