创建型模式
生成器/建造者模式( Builder Pattern)模式将一个复杂对象的构建(简单理解为设置对象字段值的过程)与它的表示(简单理解为创建对象引用的过程)分离,使得同样的构建过程可以创建不同的表示。
组成
建造者模式的实现需要3个参与者:要建造的对象、建造者及其具体实现、建造者的持有者。
- 要建造的对象就是Builder要创建的复杂对象;
- 建造者及其实现就是负责建造对象的Builder接口及其具体实现;
- 建造者的持有者持有Builder的引用,并调用Builder的方法,建造不同的复杂对象。
建造者模式的组成UML如图1所示:
图1. Builder模式的组成
在图1中,各个组成的作用:
- Product-产品:要创建的对象
- Builder/ConcreteBuilderA/ConcreteBuilderB/ConcreteBuilderC-建造者:Builder接口定义创建 Product 对象各个部分的方法,它的实现类提供具体实现
- Director:持有 Builder 的引用,执行方法来创建 Product 对象
协作过程如下:
- 客户创建 Director 对象,并用它所想要的 Builder 对象进行配置
- 一旦要创建 Product,Director 就调用 Builder
- Builder 创建 Product 的部件,并将部件添加到 Product 中
- 客户从 Builder 中得到 Product
应用场景
在以下情况时适合使用 Builder模式:
- 当创建复杂对象时,即被创建的对象有复杂的内部结构,比如有很多字段、字段类型多样等等,不适合使用 构造器 创建和赋值
- 对象的创建过程独立于它的组成部分,且构造过程可以导致被构造的对象有不同的表示,比如在不同业务场景下创建的对象需要给不同的字段设置值
示例代码
Eurka:InstanceInfo
InstanceInfo 是Eureka 的一个核心类,它记录了 Eureka 实例的所有信息,该有20+的字段属性。InstanceInfo 提供了全参构造器,但是在不同场景下有些字段是必需的有些不是,因此为了方便创建 InstanceInfo 对象,通常使用它的 Builder(它的静态内部类)来构造它的实例对象,Builder的主要代码如下:
java
// Builder
public static final class Builder {
@XStreamOmitField
private 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 控制,Director 可以根据需要选择设置不同的字段,创建出不同的对象实例,一些创建示例代码如下:
java
// ApplicationInfoManager
private 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();
}
// InstanceInfoFactory
public InstanceInfo create(EurekaInstanceConfig config) {
// Builder the instance information to be registered with eureka
// server
InstanceInfo.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 information
for (Map.Entry<String, String> mapEntry : config.getMetadataMap().entrySet()) {
String key = mapEntry.getKey();
String value = mapEntry.getValue();
// only add the metadata if the value is present
if (value != null && !value.isEmpty()) {
builder.add(key, value);
}
}
InstanceInfo instanceInfo = builder.build();
// ... 省略
return instanceInfo;
}
// EurekaConfigBasedInstanceInfoProvider
public 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() 方法。
为了解决这个问题,有一种"通用Builder"的实现方式。
使用 Java 的泛型、Supplier、Consumer等特性,实现了对不同Product类的通用创建,通用Builder的示例代码如下:
java
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 <P> 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
*/
@FunctionalInterface
public 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 模式,以链式的方式构造对象。