@XingdingCAO
2017-11-01T12:12:38.000000Z
字数 5154
阅读 2101
Java
constructor
builder
上一节讨论了原生构造器的不足,所以使用static factory method
就在某些情况下具有了一定的优越性。但是,在面对大量的参数时二者都有点儿力不从心,所以前辈们创造了新的方法来供我们参考和学习。
// 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; // optional
private final int fat; // (g) optional
private final int sodium; // (mg) optional
private final int carbohydrate; // (g) 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;
}
}
— 从上面的例子可以看出,望远镜模式就是将必须赋值的字段都放入到构造方法中,那些可选的字段呢,就通过多个构造方法覆盖。
— 显而易见的缺点就是,可读性太差,使用者必须翻阅冗长的文档才能构造一个对象。而且,由于多个参数的排列,使用者非常容易混淆参数的序列,从而造成无法被编译器检测到的bug。
// 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; // " " " "
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;}
}
— 这个模式没有了上个模式的缺点,虽然还是有一点儿啰嗦,但是可读性已经大大提高。
— 可是,这个模式也有着致命的缺点,对象的实例在构造时必须一次次调用setter
方法,使得构造时的实例存在着不一致的状态。在上个模式中,可以通过一致的参数验证来防止不一致性的产生,但是在这个模式中仅仅通过单独的参数验证已经无法实现了。
— 如果使用了不一致状态下的实例,就容易产生无法被检测的bug,导致程序的崩溃。
// 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 carbohydrate = 0;
private int sodium = 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 carbohydrate(int val){ carbohydrate = val; return this;}
public Builder sodium(int val){ sodium = 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;
}
}
— builder
模式是这样解决不一致性的:不直接构造实例,而是通过调用static factory
或constructor
方法,并传递所有需要的参数,从而得到一个builder
对象。然后使用者通过调用builder
对象中的setter-like
的方法,将一个个参数传递给builder
对象。最后,调用builder
对象中的build
方法,最后得到构造好的实例。
builder
对象在类中是一个static member class
静态内部类,也就是说,其功能被整个类共享。builder
对象中的字段与类中的字段一一对应。类中的字段都被private
final
修饰,构造方法也被private
修饰,这些做法共同去保证类的不变性。而builder
对象中的字段都被private
修饰,必需的字段被final
修饰,可选字段则被赋予默认值。因为被final
修饰的常量只能直接赋予默认值或者在构造方法中赋值,所以可选字段为非常量;又因为与可选字段对应的setter-like
方法可能不会被调用,所以每个可选字段都有默认值。builder
对象中的setter-like
的方法的返回值是对象本身的引用,这样使得方法可以被连续调用。builder
对象的唯一公有构造方法必须传递两个参数,而这两个参数是类的必需字段。build
方法应该实施对所有参数的校验,以保证返回合法的实例。如果某个参数不合法,应及时抛出IllegaStateException
,并且将出错的参数的字段名输出到异常的详细信息中。或者,在每个setter-like
的方法中都设置参数验证,并及时抛出异常。千万不能在创建实例后再去校验,这样极易产生严重的bug。
NutritionFacts nutritionFacts = new NutritionFacts.Builder(100,200)
.calories(2000)
.carbohydrate(200)
.fat(500)
.sodium(250)
.build();
builder pattern
的优缺点Javabeans pattern
的不一致性的问题,更加安全可靠。尤其在面对多线程访问时,一致的状态更加高效,减轻同步与互斥对性能的影响(例如:不变类)。builder
对象可以用来构造多个实例,而且builder
对象可以根据创建实例的不同而自动地变化,比如:可以通过builder
对象自动地给每一个实例添加一个序列号码。telescoping pattern
的构造方法只能拥有一个varargs
可变参数,builder patter
的多个setter-like
方法都可以拥有独自的varargs
。 优点
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
方法则修复了这些不足。缺点
创建builder
对象带来了额外的开销,而且这个开销通常难以被人注意到。在某些性能优先的情况下,这样的开销可能是非常拖后腿的。
telescoping pattern
,builder patter
有一点过于啰嗦(对于编码人员来说啰嗦的builder patter
却更直观)。所以,如果字段不是特别多的时候,telescoping pattern
也非常合适;大概在超过四个字段时,再去考虑builder pattern
是个不错的选择。但是,你必须考虑到后续可能的升级维护,如果你选择了前者,你就无法再将其升级为后者了(兼容旧的代码)。终上所述,在一开始就选择builder pattern
也合情合理。