builder

第四章 建造者模式(Builder Pattern)

1 概要

建造者模式是一个创建模式(Creational Design Patterns)。它能有效的帮助开发人员创建对象。起初,建造者模式是用来解决构造函数过多的问题的。但是,因为建造者模式设计简单、使用方便,它已慢慢地变成一种常用的构造对象的方法。

2 问题的起源

假设小水滴系统中有一个Teacher类,用于表示一名老师。Teacher类中包括姓名(name)、年龄(age)、专业(major)、电话号码(phone)和地址(address)。其中,姓名、年龄和专业属性为必选属性。电话号码和地址为可选属性。

开发人员可以通过使用姓名、年龄和专业来创建一名Teacher对象,如下面的例子所示。

public class Teacher {
    private String name = null;
    private Integer age = null;
    private String major = null;
    private String phone = null;
    private String address = null;

    public Teacher(String name, Integer age, String major) {
        this.name = name;
        this.age = age;
        this.major = major;
    }
}

然而,有时开发人员还希望传入电话号码和地址。因此,开发人员可能再设计两个新的构造函数,如下所示。

public class Teacher {
    ...
    public Teacher(String name, Integer age, String major, String phone) {
        this(name, age, major);
        this.phone = phone;
    }

    public Teacher(String name, Integer age, String major, String phone, String address) {
        this(name, age, major, phone);
        this.address = address;
    }
}

随着项目的推进,小水滴可能还会在Teacher类中添加新的属性,例如性别、入职时间、工作经验等。那么,Teacher类随之会出现更多的构造函数。

面对这么多的构造函数,开发人员可能会遇到如下问题。

  1. 在什么场景下使用哪个构造函数?
  2. 当选择一个构造函数后,Teacher类中各成员变量的值是多少?哪些属性会使用默认值?
  3. 构造函数中的参数对应哪些成员变量?

3 建造者模式的结构

为了解决上述的问题,开发人员设计了建造者模式。它能够帮助开发者构造复杂的对象。开发者可以根据业务逻辑逐步的设置对象属性,并构造出对象。

建造者模式包含四个参与方。

  1. Product是开发人员需要创建的对象。在本例中为Teacher类。
  2. Builder是建造者接口。开发人员使用Builder接口创建Product对象。
  3. ConcreteBuilder。建造者实现类,用于实现创建Product对象的逻辑。
  4. Director使用Builder接口,创建Product对象。

图一 建造者模式结构图

图一 建造者模式结构图

4 建造者模式的示例

我们还是以Teacher为例介绍建造者模式的使用方法。我们需要创建的对象是Teacher对象,如下面的代码所示。Teacher类还定义了一个接收所有成员的构造函数。

public class Teacher {
    private String name = null;
    private Integer age = null;
    private String major = null;

    public Teacher(String name, Integer age, String major) {
        this.name = name;
        this.age = age;
        this.major = major;
    }
}

我们再定义Teacher类的建造接口。针对Teacher类中的每一个成员,TeacherBuilder都设有一个方法来设置成员变量。例如,withName()方法用于设置成员变量name。这些方法的返回类型都是TeacherBuilder,这是为了方便的将方法调用串联起来,组成方法调用链。最后,build()方法会创建并返回Teacher对象。

public interface TeacherBuilder {
    public TeacherBuilder withName(String name);
    public TeacherBuilder withAge(Integer age);
    public TeacherBuilder withMajor(String major);
    public Teacher build();
}

我们再定义一个具体的ConcreteTeacherBuilder类。

public class ConcreteTeacherBuilder implements TeacherBuilder {
    private String name = null;
    private Integer age = null;
    private String major = null;

    public TeacherBuilder withName(String name) {
        this.name = name;
        return this;
    }

    public TeacherBuilder withAge(Integer age) {
        this.age = age;
        return this;
    }

    public TeacherBuilder withMajor(String major) {
        this.major = major;
        return this;
    }

    public Teacher build() {
        return new Teacher(this.name, this.age, this.major);
    }
}

最后,我们再来看看如何使用建造者模式创建一个Teacher对象。值得注意的是,Teacher类只有一个构造函数。但是,在使用ConcreteTeacherBuilder时,开发人员可以传入任意个属性的值来创建Teacher对象。因此,当Teacher类的属性逐渐增多时,以下的代码仍然是逻辑清晰完整的。

public class CreateTeacherExample {
    public Teacher createTeacherAdam(TeacherBuilder builder) {
        // 使用姓名属性创建Teacher对象。
        return builder.withName("Adam").build();
    }

    public Teacher createTeacherJulie(TeacherBuilder builder) {
        // 使用姓名和年龄属性创建Teacher对象。
        return build.withName("Julie").withAge(35).build();
    }

    public Teacher createTeacherDavid(TeacherBuilder builder) {
        // 使用姓名、年龄和专业属性创建Teacher对象。
        return build.withName("David").withAge(40).withMajor("CS").build();
    }
}

5 应用举例

建造者模式广泛的应用于各种项目之中。我们例举三个典型的应用来进一步解释建造者的设计与实现方法。

5.1 创建只读对象(Immutable Object)

建造者模式可以用于创建只读对象(Immutable Object)。我们先将上述Teacher的例子转换为一个使用建造者模式创建只读Teacher对象的例子。

如下面的例子所示,Teacher类的构造函数被设置为私有构造函数。因此,Teacher对象只能由Teacher.Builder类创建。Teacher类的三个成员变量(name、age和major)全部为私有成员变量(Private Member Fields),并且没有公开(Public)的setter方法设置这些成员变量。因此,当Teacher对象被创建后,外部代码无法再次修改它们的值。

public class Teacher {
    private String name = null;
    private Integer age = null;
    private String major = null;

    // 私有构造函数
    private Teacher(String name, Integer age, String major) {
        this.name = name;
        this.age = age;
        this.major = major;
    }

    public static Teacher.Builder builder() {
        return new Teacher.Builder();
    }

    public static class Builder {
        private String name = null;
        private Integer age = null;
        private String major = null;

        public Builder withName(String name) {
            this.name = name;
            return this;
        }

        public Builder withAge(Integer age) {
            this.age = age;
            return this;
        }

        public Builder withMajor(String major) {
            this.major = major;
            return this;
        }

        public Teacher build() {
            return new Teacher(this.name, this.age, this.major);
        }
    }

    public static void main(String[] args) {
        // teacher对象为只读对象
        Teacher teacher = Teacher.builder().withName("David").withAge(40).withMajor("CS").build();
    }
}

guava是由谷歌(Google)开发的开源代码库。它实现了一些常用的功能。其中,ImmutableList类也使用了建造者模式。我们在下面的例子中展示了ImmutableList.java的部分源代码,以便于阅读建造者模式的实现细节。

ImmutableList的建造者模式的实现方式与我们上述的Teacher类的例子十分相似。ImmutableList类的内部有一个Builder类。ImmutableList的静态方法builder()返回一个Builder对象。这个Builder对象在build()成员方法中创建并返回ImmutableList对象。在整个创建过程中,Builder对象保存了整个过程的中间数据。

// com.google.common.collect.ImmutableList.java from guava 30.1
package com.google.common.collect;

import java.util.List;
import java.util.RandomAccess;

public abstract class ImmutableList<E> extends ImmutableCollection<E>
    implements List<E>, RandomAccess {
    ...
    public static <E> Builder<E> builder() {
        return new Builder<E>();
    }

    ...
    public static final class Builder<E> extends ImmutableCollection.Builder<E> {
        @VisibleForTesting Object[] contents;
        private int size;
        ...

        public Builder() {
            this(DEFAULT_INITIAL_CAPACITY);
        }

        ...
        @CanIgnoreReturnValue
        @Override
        public Builder<E> add(E element) {
            checkNotNull(element);
            getReadyToExpandTo(size + 1);
            contents[size++] = element;
            return this;
        }

        @CanIgnoreReturnValue
        @Override
        public Builder<E> add(E... elements) {
            checkElementsNotNull(elements);
            add(elements, elements.length);
            return this;
        }

        ...
        @Override
        public ImmutableList<E> build() {
            forceCopy = true;
            return asImmutableList(contents, size);
        }
    }
}

上述的ImmutableList继承自ImmutableCollection类。在ImmutableCollection中,add()方法、remove()方法或者其他可能改变集合数据的方法会直接抛出UnsupportedOperationException对象。因此,当开发人员尝试修改ImmutableList对象时,会收到UnsupportedOperationException。

// com.google.common.collect.ImmutableCollection.java from guava 30.1
public abstract class ImmutableCollection<E> extends AbstractCollection<E> implements Serializable {
    ...
    @CanIgnoreReturnValue
    @Deprecated
    @Override
    public final boolean add(E e) {
        throw new UnsupportedOperationException();
    }

    ...
    @CanIgnoreReturnValue
    @Deprecated
    @Override
    public final boolean remove(Object object) {
        throw new UnsupportedOperationException();
    }
    ...
}

ImmutableList对象可由下面的代码创建。

import com.google.common.collect.ImmutableListExample;

public class ImmutableListExample {
    public static void main(String[] args) {
        List<String> list = ImmutableList<String>.builder().add("The first element").build();
    }
}

5.2 Java标准库中的建造者

Java标准库(Java Standard Library)中也使用了建造者模式。例如,Java标准库提供了建造者类Calendar.Builder、Locale.Builder来创建Calendar和Locale对象。我们简要的介绍一下Locale.Builder的实现细节。

其实,Locale.Builder的实现细节与前述的例子相似。Locale类的内部设置了Locale.Builder类。在Locale.Builder类中有一组set*()方法,用于设置Locale的属性。最后,build()方法会创建并返回一个Locale对象。

// java.util.Locale.java in Openjdk 15
package java.util;

public final class Locale implements Cloneable, Serializable {
    ...
    public static final class Builder {
        private final InternalLocaleBuilder localeBuilder;

        public Builder() {
            localeBuilder = new InternalLocaleBuilder();
        }

        ...
        public Builder setLanguage(String language) {
            try {
                localeBuilder.setLanguage(language);
            } catch (LocaleSyntaxException e) {
                throw new IllformedLocaleException(e.getMessage(), e.getErrorIndex());
            }
            return this;
        }

        public Builder setRegion(String region) {
            try {
                localeBuilder.setRegion(region);
            } catch (LocaleSyntaxException e) {
                throw new IllformedLocaleException(e.getMessage(), e.getErrorIndex());
            }
            return this;
        }

        ..
        public Locale build() {
            BaseLocale baseloc = localeBuilder.getBaseLocale();
            LocaleExtensions extensions = localeBuilder.getLocaleExtensions();
            if (extensions == null && baseloc.getVariant().length() > 0) {
                extensions = getCompatibilityExtensions(baseloc.getLanguage(), baseloc.getScript(),
                        baseloc.getRegion(), baseloc.getVariant());
            }
            return Locale.getInstance(baseloc, extensions);
        }
    }
}

下面的代码展示了如何使用建造者创建Locale对象的例子。其使用方法与前述的例子相同。

import java.util.Locale;

public class LocaleExample {
    public static void main(String[] args) {
        Locale locale = new Locale.Builder().setLanguage("en").setRegion("US").build();
        ...
    }
}

5.3 Lombok自动创建建造者

Lombok库使用Java标注(Java Annotation)来帮助开发人员自动生成源代码。使用Lombok的好处是它能帮助开发人员自动生成代码,保持代码简洁、降低开发的工作量。但是,其缺陷也是因为它会自动生成代码。如果自动生成的代码出现问题的话,会非常难以定位问题。所以,使用Lombok库需要谨慎考虑。

当使用Lombok库中@Builder标注时,Lombok库会自动的在类中生成Builder类。如下面的代码所示,如果我们定义了一个Teacher类;在类中定义三个成员变量name、age和major。

import lombok.Builder;
import java.util.Set;

@Builder
public class Teacher {
    private String name = null;
    private Integer age = null;
    private String major = null;
}

在项目编译过程中,Lombok会将上述的代码转换为下面的代码。主要的变化是添加了TeacherBuilder类。如果对Lombok库如何处理标注和生成代码感兴趣的话,可参考小水滴Java标注的文章中标注处理(Annotation Processing)章节

public class Teacher {
    private String name = null;
    private Integer age = null;
    private String major = null;
  
    Teacher(String name, Integer age, String major) {
        this.name = name;
        this.age = age;
        this.major = major;
    }
  
    public static TeacherBuilder builder() {
        return new TeacherBuilder();
    }
  
    public static class TeacherBuilder {
        private String name = null;
        private Integer age = null;
        private String major = null;
    
        TeacherBuilder() {
        }
    
        public TeacherBuilder name(String name) {
            this.name = name;
            return this;
        }
    
        public TeacherBuilder age(Integer age) {
            this.age = age;
            return this;
        }
    
        public TeacherBuilder major(String major) {
            this.major = major;
            return this;
        }

        public Teacher build() {
            return new Teacher(name, age, major);
        }
    }
}

6 小结

建造者模式是一个经典的构造模式。它与抽象工厂模式非常相似。然而,抽象工厂模式侧重于一次性函数调用完成对象创建,而建造者模式侧重于使用一个流程来逐步创建对象。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.