美文网首页
Effective Java 3rd 条目2 面临众多构造子参数

Effective Java 3rd 条目2 面临众多构造子参数

作者: tigershin | 来源:发表于2017-12-29 10:34 被阅读34次

    静态工厂和构造子都有一个限制:它们不能很好调节大量可选参数。考虑这个情况,一个类表示包装食品上的营养成分标签。这些标签有些是必需的域,食用份量、份数和每份卡路里数;还有二十多个可选域,总脂肪含量、饱和脂肪含量、反脂肪含量、胆固醇含量,钠含量等等。大多数产品中这些可选参数只有很少的非零值。

    对于这样的类,你打算用什么类型的构造子或者静态工厂?习惯上,编程人员用重叠构造子(telescoping constructor)模式:提供仅仅必需参数的构造子,有单个可选参数的另外一个构造子,和有两个可选参数的第三个构造子,等等,最后有所有可选参数的构造子。这是实践中的样子。为了简单起见,只展示四个可选参数:

    // Telescoping constructor pattern - does not scale well!
    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);
    

    通常,调用这样的构造子需要你不想设置的参数,但是你被迫为它们传递参数。这种情况下,你传递0这个值给fat。对于“只有”六个参数的情况,这个看上去不算很糟,但是当参数数量增加时,很快就会失控。

    简而言之,重叠构造子模式奏效,但是当有很多参数的时候,写客户端代码是困难的,读代码更困难。阅读的人会想,这些值是什么,而且必须细心计算参数来弄明白。相同类型参数的长长的系列会造成微妙的bug。如果客户端不慎颠倒了两个这样的参数,编译器不会发现,但是程序在运行时异常(条目51)。

    当面临许多可选参数的构造子时,第二个替代方案是JavaBean模式,这个模式中,你调用一个无参数的构造子来创建一个对象,然后调用设置方法来设置每个必需的参数和感兴趣的可选参数:

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

    这个模式没有重叠构造子模式的缺点。虽然有点啰嗦,但是容易创建实例,代码也容易阅读:

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

    不幸的是,JavaBean模式有它自己的严重缺点。因为构造被分成多个调用,一个JavaBean在构造的过程中有可能不一致的状态。这个类没有这个选择:检查构造子参数的有效性来实行一致性。尝试使用一个不一致状态的对象,会导致失败,这个失败与含有bug的代码大相径庭,所以难于调试。一个相应的缺点是,JavaBean模式限制了生成一个不变类的可能性,需要编码者花额外的功夫保证线性安全。
    手动“冻结(freezing)”对象来减少这些缺点是可能的:当对象构造完成了,不允许使用直到冻结。但是这个变体是笨拙的,在实践中用的非常稀少。此外,在运行中会造成错误,因为在使用它之前,编译器不能保证编码者调用一个对象的冻结方法。
    幸运的是,有第三种选择,可以结合重叠构造子模式的安全性和JavaBean模式的可读性。这个是Builder模式[Gamma95]的一种形式。不是直接生成对象,而是客户端调用有所有必需参数的一个构造子(或者静态工厂),获得一个builder对象。然后客户端在builder对象上调用类似设置方法,分别设置感兴趣的可选参数。最后,客户端调用无参数的build方法来生成对象,这个对象一般是不可变的。builder是一个它构建的类的静态成员类(条目24)。下面是在实践中的样子:

    // 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的设置方法返回builder自身,可以链式调用,所以有流畅的API。下面是客户端代码的样子:

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

    客户端代码容易写,更重要的是也容易通读。builder模式模仿了在Python和Scala中有名字的可选参数

    为了简单,省略了有效性检查。为了尽快发现无效的参数,在builder构造子和方法中检查参数的有效性。build方法调用的有多个参数的构造子,检查这些不变量。为了保证这些不变量不受攻击,在拷贝builder的参数后(条目50),务必对对象的域进行检查。如果检查失败,抛出IllegalArgumentException,它的具体信息表示哪些参数是无效的(条目75)。

    builder模式非常适合类继承。使用并行的builder的层级,每个builder嵌套在对应的类中。抽象的类有抽象的builder;具体的类有具体的builder。比如,考虑一个抽象类是代表各种种类披萨的层级的根类:

    // 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是一个有递归类型参数(recursive type parameter)(条目30)的泛型(generic type)。这个和抽象的self方法一起,可以在子类中使得链式方法奏效,而不需要强行转换。Java缺少self类型,这个变通方案被认为是模拟self类型的惯例。

    Pizza有两个具体的子类,一个是纽约类型的比萨,另外一个是半月比萨。前者有指定大小的参数,而后者需要指定酱汁是在外面还是里面。

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

    注意, 每个子类builder中的build方法是声明返回正确的子类的:NyPizza.Builderde的build方法返回NyPizza,而Calzone.Builder的build方法返回Calzone。这个技巧,一个子类方法被声明返回一个声明在超类中返回类型的子类型,叫做协变返回类型(covariant return typing)。这些层级builder的客户端代码,本质上与简单的NutritionFacts builder代码是等同的。

    客户端的代码例子如下,为了简洁,假设enum常量是静态导入:

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

    相对于构造子,builder有个小优点是,builder可以有多个可变参数,因为每个参数是它自己方法中指定的。或者,build可以把传入到多次调用的方法的参数聚合到单个域,就像前面addTopping方法展示的一样。

    builder模式相当灵活。单个builder可以重复使用到构建多个对象。随着创建对象的不同,builder的参数在build方法的调用之间可以改变。对象创建时builder可以自动填充到域中,比如随着每次创建时一个增加的系列数。

    builder模式也有缺点,为了创建一个对象,必须首先创建它的builder。创建builder的代价在实践中虽然是不显而易见,但是在性能要求苛刻的情形中是一个问题。而且,builder模式相对于重叠构造子模式更加啰嗦,所以如果有足够的参数,比如四个或者更多,才值得使用。但是记住,未来你有可能添加更多的参数。但是,如果你开始使用构造子或者静态工厂,然后当类演变到参数个数失控的时候,切换到builder,这时废弃的构造子或者静态工厂一直存在,这是个尴尬的事情。

    总之,在设计类时,当它的构造子或者静态工厂有多过一些参数的时候,builder模式是一个好的选择,特别是当多个参数是可选的或者是同个类型的时候。和重叠构造子相比,客户端代码更容易阅读和编写,而且builder比JavaBeans更安全。

    相关文章

      网友评论

          本文标题:Effective Java 3rd 条目2 面临众多构造子参数

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