建造者模式是一个创建模式(Creational Design Patterns)。它能有效的帮助开发人员创建对象。起初,建造者模式是用来解决构造函数过多的问题的。但是,因为建造者模式设计简单、使用方便,它已慢慢地变成一种常用的构造对象的方法。
假设小水滴系统中有一个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类随之会出现更多的构造函数。
面对这么多的构造函数,开发人员可能会遇到如下问题。
为了解决上述的问题,开发人员设计了建造者模式。它能够帮助开发者构造复杂的对象。开发者可以根据业务逻辑逐步的设置对象属性,并构造出对象。
建造者模式包含四个参与方。
图一 建造者模式结构图
我们还是以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();
}
}
建造者模式广泛的应用于各种项目之中。我们例举三个典型的应用来进一步解释建造者的设计与实现方法。
建造者模式可以用于创建只读对象(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();
}
}
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();
...
}
}
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);
}
}
}
建造者模式是一个经典的构造模式。它与抽象工厂模式非常相似。然而,抽象工厂模式侧重于一次性函数调用完成对象创建,而建造者模式侧重于使用一个流程来逐步创建对象。
注册用户登陆后可留言