写在前面
本文对应原书条目2,主要探讨的是在一个类的成员变量较多时,如何用一种可读性和可扩展性都更好的方式构建对象。通过可伸缩构造方法模式
-JavaBeans模式
-Builder模式
的探索,我们可以看到不同模式的利弊,以及为什么大部分情况下Builder模式最适用。本文的示例代码均来自原书第3版,如有版权问题,请联系我删除。
可伸缩构造方法模式
当一个类的成员变量过多时,程序员们最先想到了这个方法。这种模式就像望远镜一样,你可以按需伸展它,直到你需要的长度。首先提供一个仅含必选参数的构造方法,然后提供含一个可选参数的构造方法,接着提供更多一个可选参数的构造方法,直到涵盖所有参数为止。正如以下示例所示。
// 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;
}
}
这种方式在参数规模较小时还适用,但是当规模增大以后,需要写大量的构造方法,且在调用时容易混淆,很容易把同类型参数的顺序搞错。这种模式的客户端代码(这里客户端的概念是调用上述API的程序)可读性很差,且不便于debug。
JavaBeans模式
JavaBeans模式也是一种传统的构造对象的方式。它会提供一个无参的构造方法(当然我觉得包含必要参数也是可以的)来构造对象,然后对所有参数提供setter方法来进行设置。这种方式写成的API虽然冗长,但是易于理解,而且对应的客户端代码可读性也很强。
但是这种方式有一个很严重的缺陷,就是将对象的构造过程分割成了多次调用,使得在构造过程中对象可能处于不一致
状态。
如何理解这个不一致
呢?我的理解是每一次构建对象都不能保证有齐全的参数。比如说有一个类,有A和B两个成员变量,如果采用JavaBeans模式,那么有可能在一个地方set了A,没有set B,而另一个地方set了B没有set A。但是使用这个对象的客户端代码不知道它到底被set了哪些属性,这样就不能冒然get了。
另外一个问题是,当一个对象被多线程共享,把参数的设置权限这样放开出去也有相当大的风险,这个对象的内部基本上就会完全乱套了。
因为不一致问题,你无法用JavaBeans模式来构建不可变对象,而且还需要自行保证线程安全性。
Builder模式
最后来说说我们的主角——Builder模式
。Builder模式兼具可伸缩构造方法模式的安全性和JavaBeans模式的可读性。这种模式通常在一个类的内部加入一个静态成员类Builder,用Builder打造出一个封闭的构造区域,在这个构造区域内你可以设置必需的参数,然后像JavaBeans模式那样任意设置可选参数(这个过程是链式调用的,非常畅快),最后调用build方法拿到要构建的对象。下面是采用Builder模式的NutritionFacts类的写法:
// 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;
}
}
可以看到这个类是一个不可变类,示例代码中省略了参数有效性的检查。实际的检查可以放在Builder类的构造方法和build方法调用的NutritionFacts构造方法中。
现在要构造NutritionFacts的过程变得非常简单易读,下面是实际的用法示例:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
Builder模式适用于类层次结构
。抽象类可以有抽象类的Builder,继承它的具体的类也有自己的Builder。考虑到篇幅我就不把相关的示例代码贴出来了。感兴趣的朋友可直接阅读原书。
总结
Builder模式具有良好的可伸缩性和可读性,也能保证一致性。因为Builder是类的静态内部类,所以可重复使用来构建多个对象,而且可以在每次创建时添加一下自动填充的属性,比如序列号。
Builder模式也有缺点。一个是在创建对象之前必须先创建相应的builder,这在对性能要求很严苛的情况下会有问题(不过一般也不太会有这么极致的要求吧......)。还有一个问题是,Builder模式还是比较冗长的,所以建议只有在参数数量较多(四个以上)时再使用。
不过也要注意,如果在设计类的起初就预料到类的规模在将来会变得很庞大,那最好一开始就采用Builder模式,省得后面再改麻烦。
最后的最后,lombok的@Builder注解是个好东西啊,如果你渴望Builder模式的便利,又懒得自己写那么多重复代码,@Builder绝对会是你的不二之选:)
声明
本文仅用于学习交流,请勿用于商业用途。转载请注明出处,如果涉及任何版权问题,请及时与我联系,谢谢!
参考资料
- 《Effective Java(第3版)》
- Java实例变量和类变量 https://blog.csdn.net/itmyhom...
- 【Effective Java】理解 - 在构造过程中JavaBeans可能处于不一致的状态 https://blog.csdn.net/digi352...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。