[关闭]
@XingdingCAO 2017-11-01T12:12:38.000000Z 字数 5154 阅读 2101

Effective Java(第2版):item 2 —— Consider a builder when faced with many constructor parameters

Java constructor builder


面对多个参数时的力不从心

上一节讨论了原生构造器的不足,所以使用static factory method就在某些情况下具有了一定的优越性。但是,在面对大量的参数时二者都有点儿力不从心,所以前辈们创造了新的方法来供我们参考和学习。

多种设计方法

1. the telescoping constructor pattern

  1. // Telescoping constructor pattern - does not scale well!
  2. public class NutritionFacts {
  3. private final int servingSize; // (mL) required
  4. private final int servings; // (per container) required
  5. private final int calories; // optional
  6. private final int fat; // (g) optional
  7. private final int sodium; // (mg) optional
  8. private final int carbohydrate; // (g) optional
  9. public NutritionFacts(int servingSize, int servings) {
  10. this(servingSize, servings, 0);
  11. }
  12. public NutritionFacts(int servingSize, int servings,int calories) {
  13. this(servingSize, servings, calories, 0);
  14. }
  15. public NutritionFacts(int servingSize, int servings,int calories, int fat){
  16. this(servingSize, servings, calories, fat, 0);
  17. }
  18. public NutritionFacts(int servingSize, int servings,int calories, int fat,int sodium) {
  19. this(servingSize, servings, calories, fat, sodium, 0);
  20. }
  21. public NutritionFacts(int servingSize, int servings,int calories, int fat,int sodium, int carbohydrate) {
  22. this.servingSize = servingSize;
  23. this.servings = servings;
  24. this.calories = calories;
  25. this.fat = fat;
  26. this.sodium = sodium;
  27. this.carbohydrate = carbohydrate;
  28. }
  29. }

— 从上面的例子可以看出,望远镜模式就是将必须赋值的字段都放入到构造方法中,那些可选的字段呢,就通过多个构造方法覆盖。
— 显而易见的缺点就是,可读性太差,使用者必须翻阅冗长的文档才能构造一个对象。而且,由于多个参数的排列,使用者非常容易混淆参数的序列,从而造成无法被编译器检测到的bug。

2. the JavaBeans pattern

  1. // JavaBeans Pattern - allows inconsistency, mandates mutability
  2. public class NutritionFacts {
  3. // Parameters initialized to default values (if any)
  4. private int servingSize = -1; // Required; no default value
  5. private int servings = -1; // " " " "
  6. private int calories = 0;
  7. private int fat = 0;
  8. private int sodium = 0;
  9. private int carbohydrate = 0;
  10. public NutritionFacts() { }
  11. // Setters
  12. public void setServingSize(int val) { servingSize = val; }
  13. public void setServings(int val) { servings = val; }
  14. public void setCalories(int val) { calories = val; }
  15. public void setFat(int val) { fat = val; }
  16. public void setSodium(int val) { sodium = val; }
  17. public void setCarbohydrate(int val) { carbohydrate = val;}
  18. }

— 这个模式没有了上个模式的缺点,虽然还是有一点儿啰嗦,但是可读性已经大大提高。
— 可是,这个模式也有着致命的缺点,对象的实例在构造时必须一次次调用setter方法,使得构造时的实例存在着不一致的状态。在上个模式中,可以通过一致的参数验证来防止不一致性的产生,但是在这个模式中仅仅通过单独的参数验证已经无法实现了。

— 如果使用了不一致状态下的实例,就容易产生无法被检测的bug,导致程序的崩溃。

3. the Builder pattern

  1. // Builder Pattern
  2. public class NutritionFacts {
  3. private final int servingSize;
  4. private final int servings;
  5. private final int calories;
  6. private final int fat;
  7. private final int sodium;
  8. private final int carbohydrate;
  9. public static class Builder {
  10. // Required parameters
  11. private final int servingSize;
  12. private final int servings;
  13. // Optional parameters - initialized to default values
  14. private int calories = 0;
  15. private int fat = 0;
  16. private int carbohydrate = 0;
  17. private int sodium = 0;
  18. public Builder(int servingSize, int servings) {
  19. this.servingSize = servingSize;
  20. this.servings = servings;
  21. }
  22. public Builder calories(int val){calories = val; return this;}
  23. public Builder fat(int val){ fat = val; return this; }
  24. public Builder carbohydrate(int val){ carbohydrate = val; return this;}
  25. public Builder sodium(int val){ sodium = val; return this; }
  26. public NutritionFacts build() {return new NutritionFacts(this);}
  27. }
  28. private NutritionFacts(Builder builder) {
  29. servingSize = builder.servingSize;
  30. servings = builder.servings;
  31. calories = builder.calories;
  32. fat = builder.fat;
  33. sodium = builder.sodium;
  34. carbohydrate = builder.carbohydrate;
  35. }
  36. }

builder模式是这样解决不一致性的:不直接构造实例,而是通过调用static factoryconstructor方法,并传递所有需要的参数,从而得到一个builder对象。然后使用者通过调用builder对象中的setter-like的方法,将一个个参数传递给builder对象。最后,调用builder对象中的build方法,最后得到构造好的实例。

几个设计要点
  1. builder对象在类中是一个static member class静态内部类,也就是说,其功能被整个类共享。
  2. builder对象中的字段与类中的字段一一对应。类中的字段都被private final修饰,构造方法也被private修饰,这些做法共同去保证类的不变性。而builder对象中的字段都被private修饰,必需的字段被final修饰,可选字段则被赋予默认值。因为被final修饰的常量只能直接赋予默认值或者在构造方法中赋值,所以可选字段为非常量;又因为与可选字段对应的setter-like方法可能不会被调用,所以每个可选字段都有默认值。
  3. builder对象中的setter-like的方法的返回值是对象本身的引用,这样使得方法可以被连续调用。
  4. builder对象的唯一公有构造方法必须传递两个参数,而这两个参数是类的必需字段。
  5. build方法应该实施对所有参数的校验,以保证返回合法的实例。如果某个参数不合法,应及时抛出IllegaStateException,并且将出错的参数的字段名输出到异常的详细信息中。或者,在每个setter-like的方法中都设置参数验证,并及时抛出异常。千万不能在创建实例后再去校验,这样极易产生严重的bug。
  1. NutritionFacts nutritionFacts = new NutritionFacts.Builder(100,200)
  2. .calories(2000)
  3. .carbohydrate(200)
  4. .fat(500)
  5. .sodium(250)
  6. .build();
builder pattern的优缺点
  1. 优点
    上文也提到了,这种方式解决了Javabeans pattern的不一致性的问题,更加安全可靠。尤其在面对多线程访问时,一致的状态更加高效,减轻同步与互斥对性能的影响(例如:不变类)。
  2. 优点
    这种方式更加灵活。一个builder对象可以用来构造多个实例,而且builder对象可以根据创建实例的不同而自动地变化,比如:可以通过builder对象自动地给每一个实例添加一个序列号码。
  3. 优点
    相比于telescoping pattern的构造方法只能拥有一个varargs可变参数builder patter的多个setter-like方法都可以拥有独自的varargs
  4. 优点

    • 一个参数设置完成的builder对象是一个很好的Abstract Factory类(Design Pattern:Reusable Object-Oriented Elements)。也就是说这样一个参数完备的builder对象可以调用一个方法去创建一个或多个类的实例。为了实现上述的模式,你需要一个类型来代表builder对象,泛型接口就可以满足了——public interface Builder<T>{ public T build(); }
    • 然后,接受builder对象的方法应该通过受限的通配符来限制builder对象的类型参数。例如,Tree buildeTree(Builder<? extends Node>{})
    • 传统的abstract factory类的实现是通过调用Class类的newInstance方法来做的(脆弱又充满着问题)。newInstance方法会试图去调用类的无参构造方法,假如调用了不存在的构造方法并不会在编译时得到错误,而是在运行时得到一个InstantiationException或者IllegalAccessException。除此之外。这个方法还会传播无参构造方法抛出的异常(即使这个方法并没有在方法名后throw这些异常)。
    • 综上所述,旧的实现违背了编译时异常检测的原则,而builder对象的build方法则修复了这些不足。
  5. 缺点
    创建builder对象带来了额外的开销,而且这个开销通常难以被人注意到。在某些性能优先的情况下,这样的开销可能是非常拖后腿的。

  6. 缺点
    相比于telescoping patternbuilder patter有一点过于啰嗦(对于编码人员来说啰嗦的builder patter却更直观)。所以,如果字段不是特别多的时候,telescoping pattern也非常合适;大概在超过四个字段时,再去考虑builder pattern是个不错的选择。但是,你必须考虑到后续可能的升级维护,如果你选择了前者,你就无法再将其升级为后者了(兼容旧的代码)。终上所述,在一开始就选择builder pattern也合情合理。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注