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?
答案 0 :(得分:70)
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();
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);
}
}
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();
免责声明:我是帖子的作者
答案 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://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。 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)
);
优势:
可能的弊端:
可能的替代方法:
我们可以使用构建函数来设置构造函数,如下所示:
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
}
}