美文网首页
ITEM 2:成员变量较多时可以考虑使用builder替代构造函

ITEM 2:成员变量较多时可以考虑使用builder替代构造函

作者: rabbittttt | 来源:发表于2019-03-29 14:07 被阅读0次

    ITEM 2: CONSIDER A BUILDER WHEN FACED WITH MANY CONSTRUCTOR PARAMETERS

    静态工厂方法和构造函数都不能解决一个问题—— 当成员变量很多时,它们没办法控制入参。例如一个标示食品营养标签的类,它包含很多成员:每餐的份量,每餐的分量,每餐的卡路里——以及20多个可选领域——总脂肪,饱和脂肪,反式脂肪,胆固醇,钠等等。大多数变量只有某些可选的几个具有非零值。
    我们应该怎样编写构造函数或静态工厂? 传统的方法是,程序员使用
    可伸缩构造函数模式,在该模式中,只提供一个具有所需参数的构造函数,另一个具有一个可选参数,第三个具有两个可选参数,依此类推,最后提供一个具有所有可选参数的构造函数。这是它在实践中的样子。为了简洁起见,只显示了四个可选字段:

    public class NutritionFacts {
      private final int servingSize; // (mL) required private final int servings; // (per container) required
      private final int calories;
      private final int fat;
      private final int sodium;
      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
        this.fat
        this.sodium
        this.carbohydrate = carbohydrate;
      } 
    }
    

    当我们想创建一个实例,我们可以选择那个包含我们需要的所有参数的、入参列表最短的那个构造函数。
    NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
    可能有些参数我们并不想set,但我们别无选择。在上面这个例子里,我们将'fat' 设置为0。6个参数看起来也许并不太糟,但当数量增加,程序就开始失控了。简单来说,虽然伸缩构造函数模式确实可以工作,但这些构造函数对用户非常不友好,难以理解和使用。用户需要仔细阅读函数签名/文档才能知道这些参数的意义。具有相同类型的长参数可能导致细微的bug,如果用户不小心将参数填错位置,编译器并不会报警,这个错误将一直隐藏直到运行时暴露。

    还有一种解决方案,当存在许多可选参数时,我们可以选择JavaBeans模式,调用一个无参数构造函数来创建对象,然后调用setter方法来设置每个必需参数和每个感兴趣的可选参数:

    public class NutritionFacts {
      // Parameters initialized to default values (if any)
      private int servingSize = -1; // Required; no default value 
      private int servings = -1; 
      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模式无法使用在不可变类上,并且需要程序员负责保证线程安全。可以在实例未完成前“冻结”它,并且“冻结”时不允许使用,但这种方式比较笨重,很少使用。此外,它可能在运行时导致错误,因为编译器不能确保程序员在使用对象之前调用冻结方法。

    幸运的是,还有第三种方法可以将伸缩构造函数模式的安全性与JavaBean模式的可读性结合起来。它是构建器模式的一种形式。
    用户调用Builder类的构造函数,先构造必须参数,再调用类setter方法填入可选参数,最后用户调用build()方法生成实例,构建器通常是它构建的类的静态成员类。下面是它在实践中的样子:

    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 类是不可变的,并且它所有的必填参数都放在一起初始化,构建器的setter方法返回构建器本身,以便可以链接调用,从而生成一个连贯的API。下面是客户端代码的样子:

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

    以上的代码很容易读写。Builder模式模拟了Python和Scala中的命名可选参数。这里省略了有效性检查,为了尽快发现无效参数,可以在builder的构造器和方法里添加check。在build方法调用的构造方法中,检查包含多个参数的不变性。要确保这些不变量不受攻击,请在从builder复制参数后检查对象字段。如果检查失败,抛出 IllegalArgumentException 异常指示哪些参数无效。
    构建器模式非常适合于类层次结构。每一层类有对应的builder,Abstract classes 有 abstract builders; concrete classes 有 concrete builders。例如,考虑一个类层次结构,最上层是一个Abstract class :Pizza

    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缺乏自类型这一事实,这种解决方法称为模拟自类型习语。
    现在这里有两个Pizza的子类,一个是 New-York-style pizza, 另一个是 calzone(半圆形烤乳酪馅饼?),前者有一个必要的大小参数,而后者可以让你指定酱汁应该在里面还是外面:

    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() 方法返回的都是子类实例,这种函数声明返回值为父类,子类实现返回值为子类的技术称为协变返回类型。它能使用户在使用时无需类型转换。这种层级构造器与上文的营养标签例子相同,下面实例客户端代码:

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

    Builder相对于Constructor的一个微小优势在于,builder可以有多个可变参数,每个参数在自己的方法里指定。或者,构建器可以将传递给方法的多个调用的参数聚合到单个字段中,如上例中的addTopping()。
    Builder模式非常灵活,一个builder可以被重复使用,构建多个的实例。参数可以在构建方法的调用之间进行调整,以更改创建的对象。构建器可以在对象创建时自动填充一些字段,比如序列号,每次创建对象时序列号都会增加。
    Builder模式也有缺点:为了创建实例,我们必须先创建它的builder。虽然在实践中创建builder的成本不太可能高,但在比较关注性能的情况下,它可能会成为一个问题。
    Builder模式也比 constructor 更冗长,所以最好在有足够多参数的情况下使用,确保我们获得的收益足以覆盖付出的代价。但是我们可能希望在将来添加更多的参数,而如果我们从构造函数或静态工厂方法开始,并在参数数量无法控制的时候切换到构建器,那么过时的构造函数或静态工厂就会像一个疼痛的拇指一样突出。因此,最好从构建器开始。
    总之,在为有多个参数的类设计构造函数或静态工厂时(尤其是当许多参数是可选的或具有相同类型时),Builder模式是一个很好的选择。使用builder比使用可伸缩构造器更容易读写客户机代码,而且构建器比Javabean更安全。

    相关文章

      网友评论

          本文标题:ITEM 2:成员变量较多时可以考虑使用builder替代构造函

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