《Effective Java 第三版》笔记之二 当构造参数很多的时候考虑使用builder

来源:https://www.datalearner.com/blog/1051525961464023

静态工厂和构造方法都有一个缺点:当有很多可选参数的时候,其扩展性并不是很好。例如,考虑这样一个类,它表示食物包装上的营养物质标签。这些标签有一部分是必须的字段——例如分量大小、每个包装容器包含的分量大小、每份物质包含的卡路里等,还有一部分是可选字段——例如总的脂肪含量、饱和脂肪含量、反式脂肪含量等等。大多数食品只有一小部分字段是非零的结果。

对于这样一个类,要如何使用构造方法或者是静态工厂方法呢?传统上,编程者可以使用重叠构造函数模式(telescoping constructor pattern),即在某个构造方法中只包含必须的字段,然后添加其他的构造方法包含其他可选字段。举个例子:假设只有4个可选字段:

public class NutritionFacts {
    private final int servingSize; // (mL) required
    private final int servings; // (per container) required
    private final int calories; // (per serving) optional
    private final int fat; // (g/serving) optional
    private final int sodium; // (mg/serving) optional
    private final int carbohydrate; // (g/serving) optional
    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }
    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    } 
    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    } 
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 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;
    }
}

当你使用这个类创建对象实例的时候,需要选择相对应的构造方法:

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

一般情况下,这个构造方法的调用会需要许多不必要的参数,但是你必须要给它一些值。例如,在上述的例子中,我们给fat传递了一个0。如果只有6个参数,这也不是一个多么难以接受的事情,但是当参数数量增长的时候,这种方式就有点难以忍受了。

简单来说,重叠构造函数模式很有效,但是当参数很多时候写起来很麻烦,阅读也不友好。用户必须仔细阅读这些方法,并小心的计算参数的数量以避免出错。很长的相同类型的参数容易导致一些微小的错误。当用户把两个参数搞反了,程序也不会报错,但实际已经是错误的了。

第二个选择是使用JavaBean的模式来解决这个问题,你可以调用一个无参数的构造函数来创建对象,然后使用set方法将所需的字段赋值,例如:

 
// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
    // Parameters initialized to default values (if any)
    private int servingSize = -1; // Required; no default value
    private int servings = -1; // Required; no default value
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;
    public NutritionFacts() { }
    // Setters
    public void setServingSize(int val) { servingSize = val; }
    public void setServings(int val) { servings = val; }
    public void setCalories(int val) { calories = val; }
    public void setFat(int val) { fat = val; }
    public void setSodium(int val) { sodium = val; }
    public void setCarbohydrate(int val) { carbohydrate = val; }
}
  1.  

这种模式没有重叠构造函数模式的缺点,而且很容易构造,对代码阅读也很友好:

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

然而,JaveBeans本身有很大的缺点。由于构造过程有多次不同的调用,因此JavaBeans可能会产生不一致的情况。例如,JavaBeans类不能只通过检查构造函数参数的有效性来保证一致性。当一个对象处于一种不一致的状态时,试图使用它可能会引起失败,这个失败很难从包含错误的代码中去掉,因此很难调试。与此相关的一个缺点是JavaBeans的模式无法创建不可变的类,因此需要编程者花费其他成本来保证线程安全。

当构造工作完成时,可以通过手动『冰冻』对象并且在冰冻完成之前不允许使用它来弥补这个缺点,但这种方式太笨重了,在实践中很少使用。而且,由于编译器不能保证程序员在使用对象之前调用了冰冻方法,因此它可能在运行时引起错误。

幸运的是,有第三种方法既保证有重叠构造函数模式的安全性,也有JavaBeans的简洁性。这就是生成器模式(Builder pattern)。客户端使用构造方法来初始化所有必要的字段,然后使用类似setter方法来构建可选参数。最终,客户端使用一个无参的builder方法来产生一个对象,通常该对象都是不可变的。Builder通常都是一个静态的成员类:

 
// Builder Pattern
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;
        // Optional parameters - initialized to default values
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }
        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);
        }
    } 
    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

这个NutritionFacts就是不可变类,所有的默认参数都在一个地方。builder的setter方法返回builder本身从而使得可以链式调用API。调用的方法如下:

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

​​​​​​​这种API很容易写,且阅读起来也很方便。生成器模式模拟了Python和Scala中命名可选参数。

为了简短起见,参数的有效性检验在这里没有写出来。为了尽快的检测到无效的参数,可以在builder的构造器和方法中检验。检查有build方法调用的构造方法涉及到多个参数的不可变量。为了防止这些不可变量收到攻击,从builder中复制参数后对对象字段进行检验。如果检测失败,抛出IllegalArgumentException异常,可以显示哪些参数是无效的。

生成器模式非常适合具有层次结构的类。使用并行的层次构造器,每一个都被嵌套在相关的类中。抽象类有抽象的builder; 具体的类有具体的builder。例如,考虑一个层次类的根节点是一个抽象类,代表了不同的pizza:

// Builder pattern for class hierarchies
public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;
    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        } 
        abstract Pizza build();
        // Subclasses must override this method to return "this"
        protected abstract T self();
    }
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // See Item 50
    }
}

注意,Pizza.Builder是一个有着递归参数的通用类型(泛型)。它和抽象的self方法一起,允许子类中的方法进行链式调用,而不需要转换。这个方法实际上是Java确实self类型的一个变通解决方案,这个类型通常称为模拟自我类型( the simulated self-type idiom)。

现在有两个具体的Pizza子类,一个代表了标准的纽约式pizza,一个是意式包馅比萨(calzone)。前者需要大小(size)这个参数,后者需要指定酱要放在里面还是外面。

public class NyPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;
    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;
        public Builder(Size size) {
        this.size = Objects.requireNonNull(size);
        } 
        @Override 
        public NyPizza build() {
            return new NyPizza(this);
        } 
        @Override 
        protected Builder self() { return this; }
    } 
    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}
public class Calzone extends Pizza {
    private final boolean sauceInside;
    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauceInside = false; // Default
        public Builder sauceInside() {
            sauceInside = true;
            return this;
        }
        @Override 
        public Calzone build() {
            return new Calzone(this);
        } 
        @Override 
        protected Builder self() { return this; }
    } 
    private Calzone(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }
}

来源:https://www.datalearner.com/blog/1051525961464023

注意到,子类的builder被声明为返回正确的类型了。NyPizza.Builder的build方法返回的是NyPizza类,而Calzone.Builder返回的是Calzone类。这种子类方法返回父类返回值的子类型称之为协变返回类型(covariant return typing)。它允许子类可以直接使用这些builders而不需要做强制转化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值