创建型模式
生成器/建造者模式(Builde Pattern)模式将一个复杂对象的构建(简单理解为设置对象字段值的过程)与它的表示(简单理解为创建对象引用的过程)分离,使得同样的构建过程可以创建不同的表示。
组成
建造者模式的实现需要3个参与者:要建造的对象、建造者及其具体实现、建造者的持有者。
- 要建造的对象就是Builder要创建的复杂对象;
- 建造者及其实现就是负责建造对象的Builder接口及其具体实现;
- 建造者的持有者持有Builder的引用,并调用Builder的方法,建造不同的复杂对象。
建造者模式的组成UML如图1所示(来自参考文献[1]):
图1. Builder模式的组成
在图1中,各个组成的作用:
- Product-产品:要创建的对象
- Builder/ConcreteBuilderA/ConcreteBuilderB/ConcreteBuilderC-建造者:Builder接口 用来定义创建 Product 对象各个部分的方法,它的实现类提供具体实现
- Director:持有 Builder 的引用,执行方法来创建 Product 对象
协作过程如下:
- 客户创建 Director 对象,并用它所想要的 Builder 对象进行配置
- 一旦要创建 Product,Director 就调用 Builder
- Builder 创建 Product 的部件,并将部件添加到 Product 中
-
应用场景
在以下情况时适合使用 Builder模式:
当创建复杂对象时,即被创建的对象有复杂的内部结构,比如有很多字段、字段类型多样等等,不适合使用 构造器 创建和赋值
- 对象的创建过程独立于它的组成部分,且构造过程可以导致被构造的对象有不同的表示,比如在不同场景下创建的对象需要给不同的字段设置值
参考文献[2]的“建造者模式”部分给出了一个Builder模式的案例,它的案例UML如图2所示:
图2. 参考文献[2]的 Builder 案例
在图2中,BuilderPatternDemo就是前面所说的 Director,MealBuilder 就是 Builder,Product 包含了比较多的一些类(不需要关注具体的类,只需要知道它们共同构成了 Product);Director 调用 Builder 创建 Product 的代码如下(来自参考文献[2]):
// Directorpublic class BuilderPatternDemo {public static void main(String[] args) {MealBuilder mealBuilder = new MealBuilder();// Product1Meal vegMeal = mealBuilder.prepareVegMeal();System.out.println("Veg Meal");vegMeal.showItems();System.out.println("Total Cost: " +vegMeal.getCost());// Product2Meal nonVegMeal = mealBuilder.prepareNonVegMeal();System.out.println("\n\nNon-Veg Meal");nonVegMeal.showItems();System.out.println("Total Cost: " +nonVegMeal.getCost());}}// Builderpublic class MealBuilder {// Builder1public Meal prepareVegMeal (){Meal meal = new Meal();meal.addItem(new VegBurger());meal.addItem(new Coke());return meal;}// Builder2public Meal prepareNonVegMeal (){Meal meal = new Meal();meal.addItem(new ChickenBurger());meal.addItem(new Pepsi());return meal;}}
比较图1和图2以及案例代码,有以不同和问题:
- 不同:没有 ConcreteBuilder,只有一个 Builder
- 问题1:不同的 Product 由 Builder 的不同方法创建,如果我们要创建新的 Product,就需要来修改 Builder,不符合“开闭原则”
- 问题2:Builder 的方法直接创建了 Product,创建的步骤不由 Director 控制,无法更改创建步骤
更好的实现方式是:不同的 ConcreteBuilder 创建不同的 Product,并且能够由 Director 来控制每一个字段(部件)的设置。
示例代码
Eurka:InstanceInfo
InstanceInfo 是Eureka 的一个核心类,它记录了 Eureka 实例的所有信息,该有20+的字段属性。InstanceInfo 提供了全参构造器,但是在不同场景下有些字段是必需的有些不是,因此为了方便创建 InstanceInfo 对象,通常使用它的 Builder(它的静态内部类)来构造它的实例对象,Builder的主要代码如下:
// Builderpublic static final class Builder {@XStreamOmitFieldprivate InstanceInfo result;private Builder(InstanceInfo result, VipAddressResolver vipAddressResolver, Function<String,String> intern) {// ...this.result = result;}// ... 省略 setXXX 方法../*** Build the {@link InstanceInfo} object.** @return the {@link InstanceInfo} that was built based on the* information supplied.*/public InstanceInfo build() {if (!isInitialized()) {throw new IllegalStateException("name is required!");}return result;}}
在使用 Builder 创建 InstanceInfo 对象(Product)时,先创建一个Builder对象,在它的内部有一个 InstanceInfo 成员变量 result,然后调用属性的 set() 方法设置 result 的属性值,最后调用 build() 方法返回result对象。
这样做的好处是,Product 的创建过程(哪些字段需要赋值,哪些不需要)完全由 Director 控制,不再像前面(参考文献[2])案例那样,由Builder直接返回创建好的 Product,一些创建示例代码如下:
// ApplicationInfoManagerprivate void updateInstanceInfo(String newAddress, String newIp) {InstanceInfo.Builder builder = new InstanceInfo.Builder(instanceInfo);if (newAddress != null) {builder.setHostName(newAddress);}if (newIp != null) {builder.setIPAddr(newIp);}builder.setDataCenterInfo(config.getDataCenterInfo());instanceInfo.setIsDirty();}// InstanceInfoFactorypublic InstanceInfo create(EurekaInstanceConfig config) {// Builder the instance information to be registered with eureka// serverInstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder();// 省略其他代码...builder.setNamespace(namespace).setAppName(config.getAppname()).setInstanceId(config.getInstanceId()).setAppGroupName(config.getAppGroupName()).setDataCenterInfo(config.getDataCenterInfo()).setIPAddr(config.getIpAddress()).setHostName(config.getHostName(false)).setPort(config.getNonSecurePort()).enablePort(InstanceInfo.PortType.UNSECURE,config.isNonSecurePortEnabled()).setSecurePort(config.getSecurePort()).enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled()).setVIPAddress(config.getVirtualHostName()).setSecureVIPAddress(config.getSecureVirtualHostName()).setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl()).setStatusPageUrl(config.getStatusPageUrlPath(),config.getStatusPageUrl()).setHealthCheckUrls(config.getHealthCheckUrlPath(),config.getHealthCheckUrl(), config.getSecureHealthCheckUrl()).setASGName(config.getASGName());// 省略...// Add any user-specific metadata informationfor (Map.Entry<String, String> mapEntry : config.getMetadataMap().entrySet()) {String key = mapEntry.getKey();String value = mapEntry.getValue();// only add the metadata if the value is presentif (value != null && !value.isEmpty()) {builder.add(key, value);}}InstanceInfo instanceInfo = builder.build();// ... 省略return instanceInfo;}// EurekaConfigBasedInstanceInfoProviderpublic synchronized InstanceInfo get() {if (instanceInfo == null) {// ...InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(vipAddressResolver);// ...builder.setNamespace(config.getNamespace()).setInstanceId(instanceId).setAppName(config.getAppname()).setAppGroupName(config.getAppGroupName()).setDataCenterInfo(config.getDataCenterInfo()).setIPAddr(config.getIpAddress()).setHostName(defaultAddress).setPort(config.getNonSecurePort()).enablePort(PortType.UNSECURE, config.isNonSecurePortEnabled()).setSecurePort(config.getSecurePort()).enablePort(PortType.SECURE, config.getSecurePortEnabled()).setVIPAddress(config.getVirtualHostName()).setSecureVIPAddress(config.getSecureVirtualHostName()).setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl()).setStatusPageUrl(config.getStatusPageUrlPath(), config.getStatusPageUrl()).setASGName(config.getASGName()).setHealthCheckUrls(config.getHealthCheckUrlPath(),config.getHealthCheckUrl(), config.getSecureHealthCheckUrl());// ...instanceInfo = builder.build();instanceInfo.setLeaseInfo(leaseInfoBuilder.build());}return instanceInfo;}
从 InstanceInfo 的创建过程可以看出,在不同的场景下,Director 可以给不同字段设置值,每一步都是可见可控的,创建出的对象由创建过程控制。
通用Builder
在上面的示例中,Builder能一步一步地创建对象,但是存在一个问题:对每一个复杂对象,我们都要创建一个对应的Builder,再提供一遍类字段的 set() 方法。
为了解决这个问题,网上有一种“Java8通用Builder”(参考文献[3])的实现方式,使用 Java 的泛型、Supplier
public class Builder<T> {private final Supplier<T> instantiator;private List<Consumer<T>> modifiers = new ArrayList<>();public Builder(Supplier<T> instant) {this.instantiator = instant;}public static <T> Builder<T> of(Supplier<T> instant) {return new Builder<>(instant);}public <P1> Builder<T> with(Consumer1<T, P> consumer, P p) {Consumer<T> c = instance -> consumer.accept(instance, p);modifiers.add(c);return this;}public T build() {T value = instantiator.get();modifiers.forEach(modifier -> modifier.accept(value));modifiers.clear();return value;}/*** 自定义 Consumer*/@FunctionalInterfacepublic interface Consumer1<T, P> {void accept(T t, P p);}}
通用Builder在创建对象时,如果要设置某个字段的值,只需要使用 with() 方法,传入对应的 set() 方法的 lambda 表达式和参数,最后使用 build() 方法执行所有方法即可。
优点和缺点
从前面的示例可以得出 Builder 模式的优点和缺点。
优点:
1、可以改变一个产品的内部表示,Director 是调用 Builder 来创建 Product 对象、设置字段值,至于 Product 是如何装配这些字段值,Director是不知道的,这样就方便后续的货站,可以使用不同的 Builder 来创建满足需要的不同的Product;
2、对构造过程可以进行更加精细的控制,Builder模式在 Director 的控制下,一步一步地构造产品,只有构造完成之后,才从 Builder 中取出(build()方法);
3、将构造代码与表示代码分开,对于复杂的类和对象,用户不需要在创建对象的同时设置它的各个字段值,用户也不需要知道对象内部的所有信息,用户只需要使用Builder提供的方法,按需设置对应的字段值。
缺点:
1、如果 Product 有多个子类,需要对应的 Builder 来创建子类对象。
总结
当不想写大量的 set() 方法时,可以使用 Builder 模式,以链式的方式构造对象。
附录. 参考文献
- Erich Gamma, Richard Helm, Ralph Johnson, Jojn Vlissides. 设计模式—可复用面向对象软件的基础[M]. 机械工业出版社,北京. 2018.11
- 软件开发设计模式
- Java8通用Builder
