本文对应原书条目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模式也是一种传统的构造对象的方式。它会提供一个无参的构造方法(当然我觉得包含必要参数也是可以的)来构造对象,然后对所有参数提供setter方法来进行设置。这种方式写成的API虽然冗长,但是易于理解,而且对应的客户端代码可读性也很强。
但是这种方式有一个很严重的缺陷,就是将对象的构造过程分割成了多次调用,使得在构造过程中对象可能处于 不一致
状态。
如何理解这个 不一致
呢?我的理解是每一次构建对象都不能保证有齐全的参数。比如说有一个类,有A和B两个成员变量,如果采用JavaBeans模式,那么有可能在一个地方set了A,没有set B,而另一个地方set了B没有set A。但是使用这个对象的客户端代码不知道它到底被set了哪些属性,这样就不能冒然get了。
另外一个问题是,当一个对象被多线程共享,把参数的设置权限这样放开出去也有相当大的风险,这个对象的内部基本上就会完全乱套了。
因为不一致问题,你无法用JavaBeans模式来构建不可变对象,而且还需要自行保证线程安全性。
最后来说说我们的主角—— 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绝对会是你的不二之选:)
本文仅用于学习交流,请勿用于商业用途。转载请注明出处,谢谢。