👉 请投票支持这款 全新设计的脚手架 ,让 Java 再次伟大!
什么是建造者设计模式
建造者设计模式是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示的一种手段。
建造者设计模式是创建者模式之一。建造者模式往往用来和工厂设计模式做类比,因为这两种设计模式在设计思路上有很相似的地方。
不过需要注意的是,搞清楚这两者的区别非常重要。当理解了两种设计模式对应的问题场景,以及分别用来解决什么样的问题以后,才算真正掌握了建造者设计模式。
建造者设计模式怎么用?
设想这样一个业务场景。
你需要设计一个类,这个类需要表示某个食品的外包装,外包装上需要显示这个食品所含有的成分。而每个食品含有的成分不同,并且有些成分是必然会印刷到包装上的,而有些成分则是某些食品所独有的。你应该怎么做?
分析上述问题,由于食品品种不同,成分不同,所以食品包装类中含有必须,非必须的成分字段。面对这样的问题,最容易想到的就是重叠构造器模式。
package builder;/*** 建造者设计模式* 包装食品标签类(不可变)* 标签中拥有2个必选参数,3个可选参数*/
public class NutritionFacts {// 必选private final int servingSize;private final int servings;// 可选private final int calories;private final int fat;private final int sodium;/*** 只有calories可选参数的构造器** @param servingSize* @param servings* @param calories*/public NutritionFacts(int servingSize, int servings, int calories) {this.servingSize = servingSize;this.servings = servings;this.calories = calories;this.fat = 0;this.sodium = 0;}/*** 构造器* 无sodium条件的构造器** @param servingSize* @param servings* @param calories* @param fat*/public NutritionFacts(int servingSize, int servings, int calories, int fat) {this.servingSize = servingSize;this.servings = servings;this.calories = calories;this.fat = fat;this.sodium = 0;}/*** 构造器* 满足所有条件的构造器** @param servingSize* @param servings* @param calories* @param fat* @param sodium*/public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {this.servingSize = servingSize;this.servings = servings;this.calories = calories;this.fat = fat;this.sodium = sodium;}/*** 客户端测试** @param args*/public static void main(String[] args) {// 非常难以阅读的代码.完全无法知道参数是什么,而且容易传错参数.NutritionFacts nf = new NutritionFacts(1, 2, 3, 0, 5);}}
重叠构造器很容易理解。我们为食品包装类设计了复数个构造器,每个构造器,都拥有必须持有的两个字段。而每一个构造器都比上一个构造器多出一个可选字段。这样用户就可以根据需要,调用不同的构造器创建不同的对象了。
重叠构造器能够解决问题,但是却有一个重大的缺陷:代码非常难以阅读,用户也相当难以使用这样的类。
原因很简单,用户在使用这样的类时,需要仔细去阅读 doc(如果有的话),还需要搞清楚到底哪个是可选哪个是必选。最后在创建对象时传参更是要小心翼翼-稍不注意,就会造成参数传递错误,并且是运行时错误,得不到任何检查。
在本例中只拥有 6 个成员变量的类还算勉强可以接受,但假设这个类的成员变量膨胀到 20 个,那么通过这种方式创建这种类的对象的体验实在是太糟糕了。
如何让参数传递既安全,代码又易读呢?
假设我们将大量参数分装为一个 javaBean,创建对象时,通过 getSet 方法来设值是否是一个可行的方法?
package builder;/*** 建造者设计模式* 包装食品标签类(可变)* 标签中拥有2个必选参数,3个可选参数*/
public class NutritionFactsBeanMode {// 必选private final int servingSize;private final int servings;// 可选private int calories;private int fat;private int sodium;/*** 构造器** @param servingSize* @param servings*/public NutritionFactsBeanMode(int servingSize, int servings) {this.servingSize = servingSize;this.servings = servings;}public int getServingSize() {return servingSize;}public int getServings() {return servings;}public int getCalories() {return calories;}public void setCalories(int calories) {this.calories = calories;}public int getFat() {return fat;}public void setFat(int fat) {this.fat = fat;}public int getSodium() {return sodium;}public void setSodium(int sodium) {this.sodium = sodium;}/*** 客户端测试** @param args*/public static void main(String[] args) {NutritionFactsBeanMode nf = new NutritionFactsBeanMode();nf.setFat(1);nf.setSodium(2);nf.setCalories(3);}
}
JavaBean 模式似乎用非常简单的方法解决了问题。但是却引发了一个致命的缺陷。食品包装对象从一个不可变对象成为了一个可变对象。在并发的逻辑中,你就不得不花心思在它的同步处理上面。这真是有点得不偿失的感觉。
那么创建这样的较复杂对象,有没有办法写出又安全,阅读性又强,用户又易用的代码呢?
答案当然是有。结合静态内部类的创建者模式就是用来解决这样的问题的。
改进一下上例的 bean 模式,我们将 bean 改为不可变类,并且不允许外部创建对象。接下来通过创建内部的建造者对象,再调用建造方法来建造外部对象。
package builder;/*** 建造者设计模式* 包装食品标签类(不可变)* 标签中拥有2个必选参数,3个可选参数*/
public class NutritionFactsBuilderPattern {// 所有的外部类中的字段都是final的,保证线程安全.// 必选private final int servingSize;private final int servings;// 可选private final int calories;private final int fat;private final int sodium;/*** 构造器* 根据内部建造者的参数来初始化对象** @param builder*/private NutritionFactsBuilderPattern(Builder builder) {this.servingSize = builder.servingSize;this.servings = builder.servings;this.calories = builder.getCalories();this.fat = builder.getFat();this.sodium = builder.getSodium();}/*** 建造者*/public static class Builder {// 必选参数private final int servingSize;private final int servings;// 可选private int calories = 0;private int fat = 0;private int sodium = 0;public int getCalories() {return calories;}public Builder setCalories(int calories) {this.calories = calories;return this;}public int getFat() {return fat;}public Builder setFat(int fat) {this.fat = fat;return this;}public int getSodium() {return sodium;}public Builder setSodium(int sodium) {this.sodium = sodium;return this;}/*** 建造者必选参数构造器** @param servingSize* @param servings*/public Builder(int servingSize, int servings) {this.servingSize = servingSize;this.servings = servings;}/*** 通过内部类创建者的builder方法,调用外部类的构造器.* 创建对象.* builder类似构造器,能够对参数加约束条件.** @return*/public NutritionFactsBuilderPattern builder() {// 可以在此对所有的参数做检查.凡是不满足条件的可以在此处就报错.这就是约束条件的checkreturn new NutritionFactsBuilderPattern(this);}}/*** 客户端测试** @param args*/public static void main(String[] args) {NutritionFactsBuilderPattern.Builder builder = new NutritionFactsBuilderPattern.Builder(1, 2);// 通过builder来创建需要的不可变对象.NutritionFactsBuilderPattern obj = builder.setCalories(2).setFat(4).setSodium(5).builder();}
}
分析示例代码,NutritionFactsBuilderPattern 是不可变类,保证了线程安全。建造者类的 set 与 get 方法保证了代码的易读性,并且在编译阶段就为用户提供了参数约束性检查。通过必须字段设置为 final 防止用户漏传必须字段。
建造者模式完美解决了我们的问题,还没有任何其他模式所带来的缺陷。所以它是当我们面临类似问题时应该优先选择的解决方案。