Implement the builder pattern prior to Java 8 has lots of tedious, nearly duplicated code; the builder itself is typically boilerplate code. Some duplicate code detectors co
Building upon this answer, here's a quasi-immutable version of the builder pattern:
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 The mutable definition for the type of object to build.
* @param The immutable definition for the type of object to build.
*/
public class GenericBuilder {
/**
* Provides the methods to use for setting object properties.
*/
private final Supplier mMutable;
/**
* Calling {@link #build()} will instantiate the immutable instance using
* the mutator.
*/
private final Function mImmutable;
/**
* Adds a modifier to call when building an instance.
*/
private final List> 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 mutator, final Function immutable ) {
mMutable = mutator;
mImmutable = immutable;
}
/**
* Starting point for building an instance of a particular class.
*
* @param supplier Returns the instance to build.
* @param 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 GenericBuilder of(
final Supplier supplier, final Function 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 The type of value used when building.
* @return This {@link GenericBuilder} instance.
*/
public GenericBuilder with(
final BiConsumer 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 );
}
}
Example usage:
final var caret = CaretPosition
.builder()
.with( CaretPosition.Mutator::setParagraph, 5 )
.with( CaretPosition.Mutator::setMaxParagraph, 10 )
.build();
When the mutator's reference is released, the state of the returned object is effectively immutable. The CaretPosition class resembles:
public class CaretPosition {
public static GenericBuilder 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;
}
// ...
From here, the CaretPosition can freely reference its internal Mutator instance, which handily provides the opportunity to avoid violating encapsulation by otherwise exposing get accessor methods on the immutable class without necessity.
This is only quasi-immutable because the values can be changed if a handle to the mutable instance is retained. Here's how immutability can be violated:
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() );
Should caret.toString() include the paragraph value, the resulting string will contain the value 17 instead of 5, thereby violating immutability. Another downside to this approach is that if validation is performed at build() time, the second call to setParagraph will not be passed through the validator.
Ways to avoid this include:
Mutator copy constructor. Copy the Mutator into a new object reference, which avoids duplicating all member variables while building a truly immutable instance of the desired type.Serializable everywhere or using a deep-copy library.The Mutator copy constructor option would resemble:
private Mutator() {
}
private Mutator( final Mutator mutator) {
mParagraph = mutator.mParagraph;
mMaxParagraph = mutator.mMaxParagraph;
}
Then the change to CaretPosition is trivial---instantiate the Mutator using its copy constructor:
private CaretPosition( final Mutator mutator ) {
mMutator = new Mutator( mutator );
}