如何在Java 8中实现建造者模式?

32

在Java 8之前实现建造者模式会有很多繁琐的、几乎重复的代码;建造者本身通常是样板代码。一些重复代码检测器认为,Pre-Java 8的建造者几乎每个方法都是其他方法的副本。

考虑以下Pre-Java 8的建造者模式:

public class Person {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class PersonBuilder {
    
    private static class PersonState {
        public String name;
        public int age;
    }
    
    private PersonState  state = new PersonState();
    
    public PersonBuilder withName(String name) {
        state.name = name;
        return this;
    }
    
    public PersonBuilder withAge(int age) {
        state.age = age;
        return this;
    }
    
    public Person build() {
        Person person = new Person();
        person.setAge(state.age);
        person.setName(state.name);
        state = new PersonState();
        return person;
    }
}

如何使用Java 8实现构建器模式?


4
如果你想创建不可变的对象(即你的类没有 setters),那么建造者模式非常有用。如果你有一个可变的类,为什么要使用建造者模式,而不是直接调用 setters 呢? - Jesper
6
由于在构造过程中可能有9个可选参数,编写每种组合的构造函数要么非常丑陋和繁琐(9^2种组合),要么是不可能的(两个参数具有相同的类型)。 - bcorso
3
那为什么不直接调用setter呢?对于可变对象,你并不需要使用建造者。在JPA实体中,没有特别的原因需要使用建造者(它们与POJOs的工作方式相同),调用建造者方法或setter方法没有任何区别,无论属性有多少。你可以通过setter设置属性,不需要构造函数。 - Jesper
5
@ Jesper,如果在构造对象时需要所有参数才能使其处于有效状态,那该怎么办?使用建造者模式可以确保对象在使用之前拥有所有必需的参数。如果仅使用setter方法,则在调用setter方法之间,对象将处于无效状态。 - bcorso
3
谢谢,我知道建造者模式。我的观点是,如果你按照SpaceTrucker在上面的代码所示字面意思来使用该模式,那么与仅创建一个Person实例并调用其setter方法相比,并没有增加多少东西。 - Jesper
显示剩余6条评论
6个回答

96

GenericBuilder

构建可变对象(不可变对象稍后讨论)的想法是使用对应实例的setter方法的方法引用。这将导致一个通用的构建器,能够构建任何具有默认构造函数的POJO - 一种构建器统治它们全部;-)

实现如下:

public class GenericBuilder<T> {

    private final Supplier<T> instantiator;

    private List<Consumer<T>> instanceModifiers = new ArrayList<>();

    public GenericBuilder(Supplier<T> instantiator) {
        this.instantiator = instantiator;
    }

    public static <T> GenericBuilder<T> of(Supplier<T> instantiator) {
        return new GenericBuilder<T>(instantiator);
    }

    public <U> GenericBuilder<T> with(BiConsumer<T, U> consumer, U value) {
        Consumer<T> c = instance -> consumer.accept(instance, value);
        instanceModifiers.add(c);
        return this;
    }

    public T build() {
        T value = instantiator.get();
        instanceModifiers.forEach(modifier -> modifier.accept(value));
        instanceModifiers.clear();
        return value;
    }
}

这个构建器是使用一个供应商来创建新实例,然后使用 with 方法指定的修改来修改这些实例。

GenericBuilder 用于 Person 的示例如下:

Person value = GenericBuilder.of(Person::new)
            .with(Person::setName, "Otto").with(Person::setAge, 5).build();

属性与进一步用法

但这个构建器还有更多发现的地方。

例如,上面的实现清除了修饰符。这可以移动到自己的方法中。因此,构建器将在修改之间保持其状态,并且很容易创建多个相等的实例。或者,根据 instanceModifier 的性质,可以创建多个不同对象的列表。例如,一个 instanceModifier 可以从递增计数器中读取它的值。

延续这个思路,我们可以实现一个fork方法,它将返回所调用的 GenericBuilder 实例的新克隆。这很容易实现,因为构建器的状态仅由 instantiatorinstanceModifiers 列表组成。从那里开始,两个构建器可以被修改使用其他 instanceModifiers。它们将共享相同的基础,构建的实例上设置一些额外的状态。

我认为最后一点在企业应用程序的单元甚至集成测试中需要重型实体时尤其有用。没有实体的上帝对象,而是有构建器。

GenericBuilder 还可以替代不同测试值工厂的需求。在我的当前项目中,有许多用于创建测试实例的工厂。代码紧密耦合到不同的测试场景,并且很难从一个测试工厂中提取部分以在稍微不同的场景中另一个测试工厂中重用。使用 GenericBuilder,这变得更容易了,因为只有特定的 instanceModifiers 列表。

为了验证创建的实例是否有效,在所有 instanceModifiers 运行后,在 build 方法中可以使用一组谓词初始化 GenericBuilder,并进行验证。

public T build() {
    T value = instantiator.get();
    instanceModifiers.forEach(modifier -> modifier.accept(value));
    verifyPredicates(value);
    instanceModifiers.clear();
    return value;
}

private void verifyPredicates(T value) {
    List<Predicate<T>> violated = predicates.stream()
            .filter(e -> !e.test(value)).collect(Collectors.toList());
    if (!violated.isEmpty()) {
        throw new IllegalStateException(value.toString()
                + " violates predicates " + violated);
    }
}

不可变对象的创建

为了使用上述方案创建不可变对象,需要将不可变对象的状态提取到一个可变对象中,并使用实例化器和构建器操作可变状态对象。然后,添加一个函数来为可变状态创建一个新的不可变实例。然而,这要求不可变对象要么像这样封装其状态,要么以这种方式进行更改(基本上是将参数对象模式应用于其构造函数)。

这与在 Java 8 之前使用构建器的方式有所不同。在那里,构建器本身就是创建新实例的可变对象。现在,我们将构建器保持在可变对象中的状态与构建器功能本身分开。

实质上
停止编写样板构建器模式并开始使用GenericBuilder提高生产力。


6
相较于更短、更直接的代码 Person value = new Person(); person.setName("Otto"); persion.setAge(5);,为何你会选择使用这个呢? - Jesper
1
@Jesper 因为我想为单元测试创建类似的对象。我会再解释一下。 - SpaceTrucker
12
建造者(builder)通常是一个可变对象,用于创建一个不可变对象(例如StringBuilderString)。由于它需要设置器(setters),因此无法用于创建不可变对象。由于您的Person类是可变的,因此PersonGenericBuilder<Person>之间没有显著差异。如果需要创建许多类似的Person对象,只需使用复制构造函数即可。 - Paul Boddington
1
我承认我喜欢GenericBuilder.of(Person::new).with(Person::setName, "Otto").with(Person::setAge, 5).build();的样子,但我认为你没有真正解决一个Person就是一个构建器(或者可以通过复制构造函数实现)这一点。当你说“将不可变对象的状态提取到可变对象中”时,这就是我已经做的——可变对象被称为构建器对象。 - Paul Boddington
1
@pbabcdefp 你的最后一句话是正确的,但是因为现在有了一个通用的构造器,所以你可以从可变对象中剥离构造器逻辑并将其变成POJO。 - SpaceTrucker
显示剩余9条评论

10
public class PersonBuilder {
    public String salutation;
    public String firstName;
    public String middleName;
    public String lastName;
    public String suffix;
    public Address address;
    public boolean isFemale;
    public boolean isEmployed;
    public boolean isHomewOwner;

    public PersonBuilder with(
        Consumer<PersonBuilder> builderFunction) {
        builderFunction.accept(this);
        return this;
    }


    public Person createPerson() {
        return new Person(salutation, firstName, middleName,
                lastName, suffix, address, isFemale,
                isEmployed, isHomewOwner);
    }
}

使用方法

Person person = new PersonBuilder()
    .with($ -> {
        $.salutation = "Mr.";
        $.firstName = "John";
        $.lastName = "Doe";
        $.isFemale = false;
    })
    .with($ -> $.isHomewOwner = true)
    .with($ -> {
        $.address =
            new PersonBuilder.AddressBuilder()
                .with($_address -> {
                    $_address.city = "Pune";
                    $_address.state = "MH";
                    $_address.pin = "411001";
                }).createAddress();
    })
    .createPerson();

参考:https://medium.com/beingprofessional/think-functional-advanced-builder-pattern-using-lambda-284714b85ed5

声明:我是本篇文章的作者


你应该在帖子中注明你从Groovy中获得了灵感。如果想继续使用这种约定,使用$作为标识符可能是需要适应的。 - SpaceTrucker
通过这种实现方式,PersonBuilder和Person类都将拥有许多相似的实例变量,如“firstName”、“lastName”等。 - Kinjal
1
如果我们有检查异常,我们该如何处理它们? - Danail Tsvetanov
我该如何强制执行一个必填参数,比如说姓名是必填的? - Gautam Tadigoppula

10

您可以查看lombok项目

对于您的情况

@Builder
public class Person {
    private String name;
    private int age;
}

它会动态生成代码
public class Person {
    private String name;
    private int age;
    public String getName(){...}
    public void setName(String name){...}
    public int getAge(){...}
    public void setAge(int age){...}
    public Person.Builder builder() {...}

    public static class Builder {
         public Builder withName(String name){...}
         public Builder withAge(int age){...}
         public Person build(){...}
    }        
}

Lombok 在编译阶段执行,并且对开发人员透明。

2
虽然这也是Java 8中正确的Builder实现,但问题特别是想获取关于如何使用Java 8新功能来实现建造者模式的信息。 - SpaceTrucker
是的,你说得对。我错过了使用Java8特性的要求。这里是我的Github上的一个构建器示例 - BuilderMonad,其中我使用Java8 lambda实现了一个构建器单子。实现方式与你的答案非常相似 :) - popcorny
因为Lombok和Jackson不兼容,所以被Downvote了。要避开Lombok!请参见https://dev59.com/kek6XIcBkEYKwwoYAfO1 - Jonas G. Drange

5
我们可以使用Java 8的Consumer函数接口来避免多个getter/setter方法。
请参考下面使用Consumer接口更新后的代码。
import java.util.function.Consumer;

public class Person {

    private String name;

    private int age;

    public Person(Builder Builder) {
        this.name = Builder.name;
        this.age = Builder.age;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Person{");
        sb.append("name='").append(name).append('\'');
        sb.append(", age=").append(age);
        sb.append('}');
        return sb.toString();
    }

    public static class Builder {

        public String name;
        public int age;

        public Builder with(Consumer<Builder> function) {
            function.accept(this);
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }

    public static void main(String[] args) {
        Person user = new Person.Builder().with(userData -> {
            userData.name = "test";
            userData.age = 77;
        }).build();
        System.out.println(user);
    }
}

请参考以下链接,了解不同示例的详细信息。

https://medium.com/beingprofessional/think-functional-advanced-builder-pattern-using-lambda-284714b85ed5

https://dkbalachandar.wordpress.com/2017/08/31/java-8-builder-pattern-with-consumer-interface/


4

基于这个答案,这里提供了建造者模式的准不可变版本:

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * Responsible for constructing objects that would otherwise require
 * a long list of constructor parameters.
 *
 * @param <MT> The mutable definition for the type of object to build.
 * @param <IT> The immutable definition for the type of object to build.
 */
public class GenericBuilder<MT, IT> {
  /**
   * Provides the methods to use for setting object properties.
   */
  private final Supplier<MT> mMutable;

  /**
   * Calling {@link #build()} will instantiate the immutable instance using
   * the mutator.
   */
  private final Function<MT, IT> mImmutable;

  /**
   * Adds a modifier to call when building an instance.
   */
  private final List<Consumer<MT>> mModifiers = new ArrayList<>();

  /**
   * Constructs a new builder instance that is capable of populating values for
   * any type of object.
   *
   * @param mutator Provides methods to use for setting object properties.
   */
  protected GenericBuilder(
      final Supplier<MT> mutator, final Function<MT, IT> immutable ) {
    mMutable = mutator;
    mImmutable = immutable;
  }

  /**
   * Starting point for building an instance of a particular class.
   *
   * @param supplier Returns the instance to build.
   * @param <MT>     The type of class to build.
   * @return A new {@link GenericBuilder} capable of populating data for an
   * instance of the class provided by the {@link Supplier}.
   */
  public static <MT, IT> GenericBuilder<MT, IT> of(
      final Supplier<MT> supplier, final Function<MT, IT> immutable ) {
    return new GenericBuilder<>( supplier, immutable );
  }

  /**
   * Registers a new value with the builder.
   *
   * @param consumer Accepts a value to be set upon the built object.
   * @param value    The value to use when building.
   * @param <V>      The type of value used when building.
   * @return This {@link GenericBuilder} instance.
   */
  public <V> GenericBuilder<MT, IT> with(
      final BiConsumer<MT, V> consumer, final V value ) {
    mModifiers.add( instance -> consumer.accept( instance, value ) );
    return this;
  }

  /**
   * Instantiates then populates the immutable object to build.
   *
   * @return The newly built object.
   */
  public IT build() {
    final var value = mMutable.get();
    mModifiers.forEach( modifier -> modifier.accept( value ) );
    mModifiers.clear();
    return mImmutable.apply( value );
  }
}

使用示例:

final var caret = CaretPosition
    .builder()
    .with( CaretPosition.Mutator::setParagraph, 5 )
    .with( CaretPosition.Mutator::setMaxParagraph, 10 )
    .build();

当修改器的引用被释放时,返回对象的状态实际上是不可变的。 CaretPosition 类类似于:

public class CaretPosition {
  public static GenericBuilder<CaretPosition.Mutator, CaretPosition> builder() {
    return GenericBuilder.of( CaretPosition.Mutator::new, CaretPosition::new );
  }

  public static class Mutator {
    private int mParagraph;
    private int mMaxParagraph;

    public void setParagraph( final int paragraph ) {
      mParagraph = paragraph;
    }

    public void setMaxParagraph( final int maxParagraph ) {
      mMaxParagraph = maxParagraph;
    }
  }

  private final Mutator mMutator;
  
  private CaretPosition( final Mutator mutator ) {
    mMutator = mutator;
  }

  // ...

从这里开始,CaretPosition 可以自由地引用其内部的 Mutator 实例,这很方便,因为这样可以避免在非必要情况下暴露对不可变类的访问器方法,同时又不违反封装性。

这只是“准不可变”的,因为如果保留了对可变实例的引用,则其值可以被更改。以下是如何违反不可变性的示例:

final var mutable = CaretPosition.builder()
    .with( CaretPosition.Mutator::setParagraph, 5 )
    .with( CaretPosition.Mutator::setMaxParagraph, 10 );
final var caret = mutable.build();
mutable.setParagraph( 17 );
System.out.println( "caret para: " + caret.toString() );

如果caret.toString()包含段落值,则生成的字符串将包含17而不是5,违反了不可变性。采用这种方法的另一个缺点是,如果在build()时执行验证,则第二次调用setParagraph将无法通过验证器。
避免这种情况的方法包括:
- 不可变的复制构造函数。 将可变成员变量复制到不可变实例中,这意味着需要复制所有成员变量。 - Mutator复制构造函数。Mutator复制到新对象引用中,这样可以在构建所需类型的真正不可变实例时避免复制所有成员变量。 - 克隆。 在构造不可变实例时克隆Mutator,这要么需要在各处实现Serializable,要么使用深度复制库。 - 库。 放弃此解决方案,转而使用Project LombokAutoValueImmutablesMutator复制构造函数选项类似于:
private Mutator() {
}

private Mutator( final Mutator mutator) {
  mParagraph = mutator.mParagraph;
  mMaxParagraph = mutator.mMaxParagraph;
}

那么针对CaretPosition的变更是微不足道的 - 使用其拷贝构造函数来实例化Mutator

private CaretPosition( final Mutator mutator ) {
  mMutator = new Mutator( mutator );
}

3

我最近尝试重新学习了Java 8中的建造者模式,并采用以下方法:

public class Person {

    static public Person create(Consumer<PersonBuilder> buildingFunction) {
        return new Person().build(buildingFunction);
    }

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    private Person() {

    }

    private Person build(Consumer<PersonBuilder> buildingFunction) {
        buildingFunction.accept(new PersonBuilder() {

            @Override
            public PersonBuilder withName(String name) {
                Person.this.name = name;
                return this;
            }

            @Override
            public PersonBuilder withAge(int age) {
                Person.this.age = age;
                return this;
            }
        });

        if (name == null || name.isEmpty()) {
            throw new IllegalStateException("the name must not be null or empty");
        }

        if (age <= 0) {
            throw new IllegalStateException("the age must be > 0");
        }

        // check other invariants

        return this;
    }
}

public interface PersonBuilder {

    PersonBuilder withName(String name);

    PersonBuilder withAge(int age);
}

使用方法:

var person = Person.create(
    personBuilder -> personBuilder.withName("John Smith").withAge(43)
);

优点:

  • 简洁的构建器接口
  • 几乎不需要样板代码
  • 构建器很好地封装了起来
  • 容易将目标类的可选属性与必选属性隔离开来(可选属性在构建器中指定)
  • 在目标类中不需要设置setter(在DDD中,通常不希望使用setter)
  • 使用静态工厂方法创建目标类的实例(而不是使用new关键字,因此可以有多个静态工厂方法,每个方法都具有有意义的名称)

可能的缺点:

  • 调用代码可能会保存传入的构建器的引用,并稍后弄乱已构建的实例,但谁会这样做呢?
  • 如果调用代码保存了传入的构建器的引用,则可能会发生内存泄漏

可能的替代方案:

我们可以设置一个具有构建函数的构造函数,如下所示:

public class Person {

    static public Person create(Consumer<PersonBuilder> buildingFunction) {
        return new Person(buildingFunction);
    }

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    private Person(Consumer<PersonBuilder> buildingFunction) {
        buildingFunction.accept(new PersonBuilder() {

            @Override
            public PersonBuilder withName(String name) {
                Person.this.name = name;
                return this;
            }

            @Override
            public PersonBuilder withAge(int age) {
                Person.this.age = age;
                return this;
            }
        });

        if (name == null || name.isEmpty()) {
            throw new IllegalStateException("the name must not be null or empty");
        }

        if (age <= 0) {
            throw new IllegalStateException("the age must be > 0");
        }

        // check other invariants
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接