小鸡爪读Effective Java记录2:遇到多个构造器参数时要考虑使用构建器

259 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

//小鸡爪 == 菜鸡

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

Preface

静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。比如用一个类表示包装食品外面显示的营养成分标签。这些标签中有几个域是必需的:每份的含量、每罐的含量以及每份的卡路里。还有超过20个的可选域:总脂肪量、饱和脂肪量、转化脂肪、胆固醇、钠,等等。大多数产品在某几个可选域中都会有非零的值

We are used to using 重叠构造器模式:telescoping constructor

eg:

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;
    }

}

but: 当有许多参数的时候,需要写的构造方法就有很多,使用的时候还要对应上参数顺序,而且也难以阅读了解

It is too complicated. So we think of JavaBeans模式

先调用一个无参构造器来创建对象,然后再调用setter方法来设置每个必要的参数,以及每个相关的可选参数

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; }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts();
        cocaCola.setServingSize(240);
        cocaCola.setServings(8);
        cocaCola.setCalories(100);
        cocaCola.setSodium(35);
        cocaCola.setCarbohydrate(27);
    }
}

but 在构造过程中,我们不知道JavaBean构造完成没有,JavaBean可能处于不一致的状态,不是不可变的,这也使得线程不安全。

Luckily,we still hava 构建器

它既能保证像重叠构造器模式那样的安全性、也能保证像JavaBeans模式那么好的可读性

what?

构建器是建造者模式的一种形式。他不直接生成想要的对象,而是利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象。然后再在builder对象是调用类似于setter的方法,来设置每个相关的可选参数。最后再调用无参的builld方法来生成通常是不可变的对象。这个builder对象通常是我们要构建的类的静态成员类。

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;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100).sodium(35).carbohydrate(27).build();
    }
}

注意

一、NutritionFacts是不可变的,所有的默认参数值都单独放在一个地方。还有个值得关注点就是builder 的设值方法返回builder本身,以便把调用链接起来,得到一个流式的API

二、示例中省略了有效性检查。如果要想尽快侦测到无效的参数,可以在builder的构造器和方法中检查参数的有效性。查看不可变量,包括bui1d方法调用的构造器中的多个参数。为了确保这些不变量免受攻击,从builder复制完参数之后,要检查对象域。如果检查失败,就抛出IlleqalArqumentException,其中的详细信息再说明哪些参数是无效的。

这样子构建器就比较完善。

and

我们也可以抽象出父类构建,完成必选参数的配置,子类自身构建相应细节实现,文中称为Buider 模式也适用于类层次结构:Builder模式也适用于类层次结构。使用平行层次结构的 builder时、各自嵌套在相应的类中。抽象类有抽象的builder,具体类有具体的builder。假设用类层次根部的一个抽象类 表示各式各样的比萨:

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();

        protected abstract T self();
    }
    
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); 
    }
}

接下来我们再创建两个具体的Pizza子类,其中一个表示经典纽约风味的比萨(NyPizza)、另一个表示馅料内置的半月型 (caizone) 比萨。前者需要一个尺寸参数,后者则要你指定酱汁应该内置还是外置:

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;
    }

    @Override 
    public String toString() {
        return "New York Pizza with " + toppings;
    }
}
public class Calzone extends Pizza {
    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauceInside = false; 

        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;
    }

    @Override
    public String toString() {
        return String.format("Calzone with %s and sauce on the %s",
                toppings, sauceInside ? "inside" : "outside");
    }
}

我们先来看看如何构建:

NyPizza pizza = new NyPizza.Builder(SMALL)
        .addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
        .addTopping(HAM).sauceInside().build();

分析一下:(土里土气的说法)

new Calzone.Builder()

创建子类构建器----创建过程中:子类构建器隐式初始化父类构建(----后这个不知道怎么描述比较规范,但是我觉得知道这点容易理解)

.addTopping(HAM)

子类构建器的父类数据域-添加必要馅料,返回this对象

.sauceInside()

子类自己方法指定酱汁应该内置还是外置

.build()

构建Calzone-->显式初始化父类披萨(这里一样不知道怎么描述比较规范):父类披萨拿到子类构建器的父类数据域-->构建Calzone显式初始自己的属性-->Calzone创建完成

感叹继承的妙

原文:

与构造器相比、builder的微略优势在于,它可以有多个可变(varargs) 参数。因为builder是利用单独的方法来设置每一个参数。此外、构建器还可以将多次词用某一个方法而传人的参数集中到一个域中,如前面的调用了两次addTopping 方法的代码所示。

Builder模式十分灵活,可以利用单个 builder构建多个对象。builder的参数可以在调用build方法来创建对象期间进行调整,也可以随着不同的对象而改变。builder可以自动填充某些域,例如每次创建对象时自动增加序列号。

other

不足: 繁琐,肉眼可见,但是如果未来需要这么多参数,通常最好一开始就是构建器。