How to implement the builder pattern in Java 8?

时间:2015-07-31 20:37:14

标签: java java-8 builder

Often I find it tedious to implement the builder pattern with pre-java-8 setups. There is always lots of nearly duplicated code. The builder itself could be considered boilerplate.

In fact there are code duplicate detectors, that would consider nearly each method of a builder made with pre-java-8 facilities as a duplicate of every other method.

So considering the following class and it's pre-java-8 builder:

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;
    }
}

How should the builder pattern be implemented using java-8 facilities?

6 个答案:

答案 0 :(得分:70)

The GenericBuilder

The idea for building mutable objects (immutable objects are addressed later on) is to use method references to setters of the instance that should be built. This leads us to a generic builder that is capable of building every POJO with a default constructor - one builder to rule them all ;-)

The implementation is this:

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;
    }
}

The builder is constructed with a supplier that creates new instances and then those instances are modified by the modifications specified with the with method.

The GenericBuilder would be used for Person like this:

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

Properties and further Usages

But there is more about that builder to discover.

For example, the above implementation clears the modifiers. This could be moved into its own method. Therefore, the builder would keep its state between modifications and it would be easy create multiple equal instances. Or, depending on the nature of an instanceModifier, a list of varying objects. For example, an instanceModifier could read its value from an increasing counter.

Continuing with this thought, we could implement a fork method that would return a new clone of the GenericBuilder instance that it is called on. This is easily possible because the state of the builder is just the instantiator and the list of instanceModifiers. From there on, both builders could be altered with some other instanceModifiers. They would share the same basis and have some additional state set on built instances.

The last point I consider especially helpful when needing heavy entities for unit or even integration tests in enterprise applications. There would be no god-object for entities, but for builders instead.

The GenericBuilder can also replace the need for different test value factories. In my current project, there are many factories used for creating test instances. The code is tightly coupled to different test scenarios and it is difficult to extract portions of a test factory for reuse in another test factory in a slightly different scenario. With the GenericBuilder, reusing this becomes much easier as there is only a specific list of instanceModifiers.

To verify that created instances are valid, the GenericBuilder could be initialized with a set of predicates, which are verified in the build method after all instanceModifiers are run.

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);
    }
}

Immutable object creation

To use the above scheme for the creation of immutable objects, extract the state of the immutable object into a mutable object and use the instantiator and builder to operate on the mutable state object. Then, add a function that will create a new immutable instance for the mutable state. However, this requires that the immutable object either has its state encapsulated like this or it be changed in that fashion (basically applying parameter object pattern to its constructor).

This is in some way different than a builder was used in pre-java-8 times. There, the builder itself was the mutable object that created a new instance at the end. Now, we have a separation of the state a builder keeps in a mutable object and the builder functionality itself.

In essence
Stop writing boilerplate builder patterns and get productive using the GenericBuilder.

答案 1 :(得分:8)

您可以查看lombok project

对于你的情况

@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 :(得分:6)

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

免责声明:我是帖子的作者

答案 3 :(得分:4)

我们可以使用Java 8的Consumer功能接口来避免多个getter / setter方法。

请使用消费者界面参考以下更新的代码。

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 :(得分:2)

this answer为基础,这是构建器模式的准不变版本:

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实例,通过不必要地将get访问器方法公开在不可变类上,可以方便地提供机会避免违反封装。

这只是准可变的,因为如果保留了可变实例的句柄,则可以更改值。这是违反不变性的方式:

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复制到新的对象引用中,这样可以避免在构建所需类型的真正不可变实例时复制所有成员变量。
  • 克隆。。在构造不可变实例时,克隆变量器,这需要在各处实现Serializable或使用deep-copy library
  • 库。将此解决方案废弃为Project LombokAutoValueImmutables

Mutator复制构造函数选项类似于:

private Mutator() {
}

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

然后对CaretPosition的更改是微不足道的-使用其复制构造函数实例化Mutator

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

答案 5 :(得分:0)

我最近尝试重新访问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)
);

优势:

  • 干净的构建器界面
  • 几乎没有样板代码
  • 构建器封装良好
  • 很容易将可选属性与目标类的必需属性分开(可选属性在构建器中指定)
  • 目标类中不需要二传手(在DDD中,您通常不需要二传手)
  • 使用静态工厂方法创建目标类的实例(而不是使用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
    }
}