在Apache Flink的源码中,有很多对象都是通过类的一个静态成员类Builder来创建的,例如org.apache.flink.api.common.operators.ResourceSpec、org.apache.flink.runtime.checkpoint.OperatorSubtaskState等,这种方式有什么好处,为什么不采用直接new的方式创建对象,在《Effective Java》第三版“ITEM 2: CONSIDER A BUILDER WHEN FACED WITH MANY CONSTRUCTOR PARAMETERS”章节中可以找到答案:
In summary, the Builder pattern is a good choice when designing classes whose constructors or static factories would have more than a handful of parameters, especially if many of the parameters are optional or of identical type. Client code is much easier to read and write with builders than with telescoping constructors, and builders are much safer than JavaBeans.
可以从上面这段话总结出如下几点:
- 构造方法有多个参数的时候推荐使用Builder模式
- Builder模式相比telescoping constructors客户端的代码更易于阅读和编写
- Builder模式相比JavaBeans更加安全
本文希望帮助读者能够理解如何使用Builder来创建对象以及这种方式的优缺点,在实际开发过程中能够善于使用。
Builder模式的使用
以ResourceSpec为例,下面代码只是抽取出必要的部分进行说明
public final class ResourceSpec implements Serializable {
private final CPUResource cpuCores;
private final MemorySize taskHeapMemory;
private final MemorySize taskOffHeapMemory;
private final MemorySize managedMemory;
private final Map<String, ExternalResource> extendedResources;
private ResourceSpec(
final CPUResource cpuCores,
final MemorySize taskHeapMemory,
final MemorySize taskOffHeapMemory,
final MemorySize managedMemory,
final Map<String, ExternalResource> extendedResources) {
checkNotNull(cpuCores);
this.cpuCores = cpuCores;
this.taskHeapMemory = checkNotNull(taskHeapMemory);
this.taskOffHeapMemory = checkNotNull(taskOffHeapMemory);
this.managedMemory = checkNotNull(managedMemory);
this.extendedResources =
checkNotNull(extendedResources).entrySet().stream()
.filter(entry -> !checkNotNull(entry.getValue()).isZero())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/** Creates a new ResourceSpec with all fields unknown. */
private ResourceSpec() {
this.cpuCores = null;
this.taskHeapMemory = null;
this.taskOffHeapMemory = null;
this.managedMemory = null;
this.extendedResources = new HashMap<>();
}
public Map<String, ExternalResource> getExtendedResources() {
throwUnsupportedOperationExceptionIfUnknown();
return Collections.unmodifiableMap(extendedResources);
}
// ------------------------------------------------------------------------
// builder
// ------------------------------------------------------------------------
public static Builder newBuilder(double cpuCores, int taskHeapMemoryMB) {
return new Builder(new CPUResource(cpuCores), MemorySize.ofMebiBytes(taskHeapMemoryMB));
}
public static Builder newBuilder(double cpuCores, MemorySize taskHeapMemory) {
return new Builder(new CPUResource(cpuCores), taskHeapMemory);
}
public static class Builder {
private CPUResource cpuCores;
private MemorySize taskHeapMemory;
private MemorySize taskOffHeapMemory = MemorySize.ZERO;
private MemorySize managedMemory = MemorySize.ZERO;
private Map<String, ExternalResource> extendedResources = new HashMap<>();
private Builder(CPUResource cpuCores, MemorySize taskHeapMemory) {
this.cpuCores = cpuCores;
this.taskHeapMemory = taskHeapMemory;
}
public Builder setCpuCores(double cpuCores) {
this.cpuCores = new CPUResource(cpuCores);
return this;
}
public Builder setTaskHeapMemory(MemorySize taskHeapMemory) {
this.taskHeapMemory = taskHeapMemory;
return this;
}
public Builder setTaskHeapMemoryMB(int taskHeapMemoryMB) {
this.taskHeapMemory = MemorySize.ofMebiBytes(taskHeapMemoryMB);
return this;
}
public Builder setTaskOffHeapMemory(MemorySize taskOffHeapMemory) {
this.taskOffHeapMemory = taskOffHeapMemory;
return this;
}
public Builder setTaskOffHeapMemoryMB(int taskOffHeapMemoryMB) {
this.taskOffHeapMemory = MemorySize.ofMebiBytes(taskOffHeapMemoryMB);
return this;
}
public Builder setManagedMemory(MemorySize managedMemory) {
this.managedMemory = managedMemory;
return this;
}
public Builder setManagedMemoryMB(int managedMemoryMB) {
this.managedMemory = MemorySize.ofMebiBytes(managedMemoryMB);
return this;
}
public Builder setExtendedResource(ExternalResource extendedResource) {
this.extendedResources.put(extendedResource.getName(), extendedResource);
return this;
}
public ResourceSpec build() {
checkArgument(cpuCores.getValue().compareTo(BigDecimal.ZERO) > 0);
checkArgument(taskHeapMemory.compareTo(MemorySize.ZERO) > 0);
return new ResourceSpec(
cpuCores, taskHeapMemory, taskOffHeapMemory, managedMemory, extendedResources);
}
}
}
通过上面的代码可以知道,使用Builder模式需要如下的步骤
- 类的构造方法声明为private
- 成员变量声明为private final
- 类里有一个静态成员类Builder
- 类里有个静态方法生成Builder对象,例如上例中的newBuilder方法
- Builder的构造方法也声明为private
- Builder与所在的类拥有相同的成员变量
- Builder的构造方法中的参数,可以认为是必填参数,其他可以认为是可选参数,所以其他成员变量都必须有默认值
- Builder里面每个setter方法返回值都是Builder
- Builder里面有个build()方法用来生成具体的ResourceSpec对象
客户端调用
看下org.apache.flink.api.common.operators.util.SlotSharingGroupUtils里面是如何生成ResourceSpec对象的,直接调用ResourceSpec.newBuilder方法设置必填参数,然后继续使用各种setter方法设置可选参数,最后调用build()方法生成ResourceSpec对象。
public class SlotSharingGroupUtils {
public static ResourceSpec extractResourceSpec(SlotSharingGroup slotSharingGroup) {
if (!slotSharingGroup.getCpuCores().isPresent()) {
return ResourceSpec.UNKNOWN;
}
Preconditions.checkState(slotSharingGroup.getCpuCores().isPresent());
Preconditions.checkState(slotSharingGroup.getTaskHeapMemory().isPresent());
Preconditions.checkState(slotSharingGroup.getTaskOffHeapMemory().isPresent());
Preconditions.checkState(slotSharingGroup.getManagedMemory().isPresent());
return ResourceSpec.newBuilder(
slotSharingGroup.getCpuCores().get(),
slotSharingGroup.getTaskHeapMemory().get())
.setTaskOffHeapMemory(slotSharingGroup.getTaskOffHeapMemory().get())
.setManagedMemory(slotSharingGroup.getManagedMemory().get())
.setExtendedResources(
slotSharingGroup.getExternalResources().entrySet().stream()
.map(
entry ->
new ExternalResource(
entry.getKey(), entry.getValue()))
.collect(Collectors.toList()))
.build();
}
}
对比telescoping constructors
所谓的“telescoping constructors”可以翻译成重叠构造方法,例如flink中DistributedCacheEntry这个类的构造方法
public static class DistributedCacheEntry implements Serializable {
public String filePath;
public Boolean isExecutable;
public boolean isZipped;
public byte[] blobKey;
/** Client-side constructor used by the API for initial registration. */
public DistributedCacheEntry(String filePath, Boolean isExecutable) {
this(filePath, isExecutable, null);
}
/** Client-side constructor used during job-submission for zipped directory. */
public DistributedCacheEntry(String filePath, boolean isExecutable, boolean isZipped) {
this(filePath, isExecutable, null, isZipped);
}
/** Server-side constructor used during job-submission for files. */
public DistributedCacheEntry(String filePath, Boolean isExecutable, byte[] blobKey) {
this(filePath, isExecutable, blobKey, false);
}
/** Server-side constructor used during job-submission for zipped directories. */
public DistributedCacheEntry(
String filePath, Boolean isExecutable, byte[] blobKey, boolean isZipped) {
this.filePath = filePath;
this.isExecutable = isExecutable;
this.blobKey = blobKey;
this.isZipped = isZipped;
}
}
可以看到,第一个构造方法只有2个必填参数,第二个构造方法在第一个的基础上增加了一个可选参数,第三个造方法在第一个的基础上增加了另一个可选参数,第四个构造方法包括了所有的参数。这种方式就是所谓的“telescoping constructors”。这种方式的弊端就在于如果参数过多会导致可读性降低,并且如果参数都为相同类型,new对象的时候,会非常容易混淆参数的顺序,导致运行时错误。
对比JavaBeans
JavaBeans的方式其实就是类中的成员变量都有getter和setter方法,这样就不能将成员变量声明为final,在调用setter方法的时候,需要考虑线程安全问题。
总结
在开发过程中需要根据实际的情况选择使用哪种方式创建对象更加的合适,Builder模式也有一些不足之处,例如代码会更加冗长。
注意:本文所说的“Builder模式”并不是设计模式中所谓的“Builder Pattern”,希望读者不要混淆。以上所有源码来自于Apache Flink 1.14.0版本。