How to implement the builder pattern in Java 8?

前端 未结 6 2082
忘了有多久
忘了有多久 2020-12-04 08:58

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

6条回答
  •  鱼传尺愫
    2020-12-04 09:14

    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:

    • Immutable copy constructor. Copy the mutable member variables into the immutable instance, which entails duplicating all member variables.
    • 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.
    • Clone. Clone the mutator when constructing the immutable instance, which requires either implementing Serializable everywhere or using a deep-copy library.
    • Library. Scrap this solution for Project Lombok, AutoValue, or Immutables.

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

提交回复
热议问题