创建对象

在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.

可以从上面这段话总结出如下几点:

  1. 构造方法有多个参数的时候推荐使用Builder模式
  2. Builder模式相比telescoping constructors客户端的代码更易于阅读和编写
  3. 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模式需要如下的步骤

  1. 类的构造方法声明为private
  2. 成员变量声明为private final
  3. 类里有一个静态成员类Builder
  4. 类里有个静态方法生成Builder对象,例如上例中的newBuilder方法
  5. Builder的构造方法也声明为private
  6. Builder与所在的类拥有相同的成员变量
  7. Builder的构造方法中的参数,可以认为是必填参数,其他可以认为是可选参数,所以其他成员变量都必须有默认值
  8. Builder里面每个setter方法返回值都是Builder
  9. 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版本。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容