问题
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?
回答1:
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
.
回答2:
You can check the lombok project
For your case
@Builder
public class Person {
private String name;
private int age;
}
It would generate the code on the fly
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 do it on the compilation phase and is transparent for developers.
回答3:
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);
}
}
Usage
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();
Refer: https://medium.com/beingprofessional/think-functional-advanced-builder-pattern-using-lambda-284714b85ed5
Disclaimer: I am the author of the post
回答4:
We can use Consumer functional interface of Java 8 to avoid multiple getter/setter methods.
Refer the below-updated code with Consumer interface.
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);
}
}
Refer the below link to know the detailed information with the different examples.
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/
回答5:
I have recently tried to revisit the builder pattern in Java 8, and I am currently using the following approach:
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);
}
Usage:
var person = Person.create(
personBuilder -> personBuilder.withName("John Smith").withAge(43)
);
Advantages:
- A clean builder interface
- Little to no boilerplate code
- The builder is well encapsulated
- It's easy to segregate the optional attributes from the mandatory attributes of the target class (the optional attributes are specified in the builder)
- No setter needed in the target class (in DDD, you generally don't want setters)
- Use of a static factory method to create an instance of the target class (instead of using the new keyword, so it's possible to have several static factory methods, each with a meaningful name)
Possible drawbacks:
- The calling code can save a reference to the passed-in builder and later screw up the mounted instance, but who will do that?
- If the calling code saves a reference to the passed-in builder, a memory leak can occur
Possible alternative:
We can setup a constructor with a building function, as follows:
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
}
}
来源:https://stackoverflow.com/questions/31754786/how-to-implement-the-builder-pattern-in-java-8