在Apache Flink的源碼中,有很多對象都是通過類的一個靜態(tài)成員類Builder來創(chuàng)建的,例如org.apache.flink.api.common.operators.ResourceSpec、org.apache.flink.runtime.checkpoint.OperatorSubtaskState等,這種方式有什么好處,為什么不采用直接new的方式創(chuàng)建對象,在《Effective Java》第三版“ITEM 2: CONSIDER A BUILDER WHEN FACED WITH MANY CONSTRUCTOR PARAMETERS”章節(jié)中可以找到答案:
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.
可以從上面這段話總結(jié)出如下幾點:
- 構(gòu)造方法有多個參數(shù)的時候推薦使用Builder模式
- Builder模式相比telescoping constructors客戶端的代碼更易于閱讀和編寫
- Builder模式相比JavaBeans更加安全
本文希望幫助讀者能夠理解如何使用Builder來創(chuàng)建對象以及這種方式的優(yōu)缺點,在實際開發(fā)過程中能夠善于使用。
Builder模式的使用
以ResourceSpec為例,下面代碼只是抽取出必要的部分進(jìn)行說明
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模式需要如下的步驟
- 類的構(gòu)造方法聲明為private
- 成員變量聲明為private final
- 類里有一個靜態(tài)成員類Builder
- 類里有個靜態(tài)方法生成Builder對象,例如上例中的newBuilder方法
- Builder的構(gòu)造方法也聲明為private
- Builder與所在的類擁有相同的成員變量
- Builder的構(gòu)造方法中的參數(shù),可以認(rèn)為是必填參數(shù),其他可以認(rèn)為是可選參數(shù),所以其他成員變量都必須有默認(rèn)值
- Builder里面每個setter方法返回值都是Builder
- Builder里面有個build()方法用來生成具體的ResourceSpec對象
客戶端調(diào)用
看下org.apache.flink.api.common.operators.util.SlotSharingGroupUtils里面是如何生成ResourceSpec對象的,直接調(diào)用ResourceSpec.newBuilder方法設(shè)置必填參數(shù),然后繼續(xù)使用各種setter方法設(shè)置可選參數(shù),最后調(diào)用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”可以翻譯成重疊構(gòu)造方法,例如flink中DistributedCacheEntry這個類的構(gòu)造方法
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;
}
}
可以看到,第一個構(gòu)造方法只有2個必填參數(shù),第二個構(gòu)造方法在第一個的基礎(chǔ)上增加了一個可選參數(shù),第三個造方法在第一個的基礎(chǔ)上增加了另一個可選參數(shù),第四個構(gòu)造方法包括了所有的參數(shù)。這種方式就是所謂的“telescoping constructors”。這種方式的弊端就在于如果參數(shù)過多會導(dǎo)致可讀性降低,并且如果參數(shù)都為相同類型,new對象的時候,會非常容易混淆參數(shù)的順序,導(dǎo)致運(yùn)行時錯誤。
對比JavaBeans
JavaBeans的方式其實就是類中的成員變量都有g(shù)etter和setter方法,這樣就不能將成員變量聲明為final,在調(diào)用setter方法的時候,需要考慮線程安全問題。
總結(jié)
在開發(fā)過程中需要根據(jù)實際的情況選擇使用哪種方式創(chuàng)建對象更加的合適,Builder模式也有一些不足之處,例如代碼會更加冗長。
注意:本文所說的“Builder模式”并不是設(shè)計模式中所謂的“Builder Pattern”,希望讀者不要混淆。以上所有源碼來自于Apache Flink 1.14.0版本。