The easiest and most complex Builder in Java



    One of the most commonly considered patterns is the Builder pattern. Basically, options for implementing the "classic" version of this pattern are considered:

    MyClass my = MyClass.builder().first(1).second(2.0).third("3").build();
    

    The pattern is simple and understandable as a stool, but some understatement is felt - either the minimal option is declared antipattern, or more complex cases are ignored. I would like to correct this point by considering the limiting cases and determining the minimum and maximum boundaries of the complexity of this pattern.

    So, consider them:

    Minimal builder or rehabilitation double brace initialization


    First, consider the minimal builder that is often forgotten - double brace initialization (
    http://stackoverflow.com/questions/1958636/what-is-double-brace-initialization-in-java , http://c2.com/cgi/ wiki? DoubleBraceInitialization ). Using double brace initialization we can do the following:

    new MyClass() {{ first = 1; second = 2.0; third = "3"; }}
    

    What do we see here?
    1. Compatibility violation equals
      What is compatibility equals? The fact is that the standard equals are something like this:

      @Override public boolean equals(Object obj) {
          if(this == obj) return true;
          if(!super.equals(obj)) return false;
          if(getClass() != obj.getClass()) return false;
          ...
      }
      

      And when compared to the inherited class, equals will return false. But we create an anonymous inherited class and intervene in the inheritance chain.
    2. Possible memory leak, as An anonymous class will hold a reference to the creation context.
    3. Initialization of fields without checks.

    Furthermore, in this way it is not possible to create immutable objects, since final fields cannot be used.

    As a result, usually double brace initialization is used to initialize composite structures. For example:

    new TreeMap() {{ put("first", 1); put(second, 2.0); put("third", "3"); }}
    

    Methods are used here, rather than direct access to fields, and equals compatibility is usually not required. So how can we use such an unreliable hack-like method? Yes, it’s very simple - allocating a separate builder class for double brace initialization.

    The code of such a builder contains only definitions of fields with set default values ​​and construction methods responsible for checking parameters and calling constructors:

    public static class Builder {
        public int    first  = -1        ;
        public double second = Double.NaN;
        public String third  = null      ;
        public MyClass create() {
            return new MyClass(
                first ,
                second,
                third
                );
        }
    }
    

    Using:

    new MyClass.Builder(){{ first = 1; third = "3"; }}.create()
    

    What do we get?
    1. Builder does not interfere with the inheritance chain - it is a separate class.
    2. Builder does not flow - its use ceases after the creation of the object.
    3. Builder can control parameters - in the method of creating an object.

    Voila! Double brace initialization rehabilitated.

    To use inheritance, Builder is divided into two parts (one with fields, the other with the creation method) as follows:

    public class MyBaseClass {
        protected static class BuilderImpl {
            public int    first  = -1        ;
            public double second = Double.NaN;
            public String third  = null      ;
        }
        public static class Builder extends BuilderImpl {
            public MyBaseClass create() {
                return new MyBaseClass(
                    first ,
                    second,
                    third
                    );
            }
        }
        ...
    }
    public class MyChildClass extends MyBaseClass {
        protected static class BuilderImpl extends MyBaseClass.BuilderImpl {
            public Object fourth = null;
        }
        public static class Builder extends BuilderImpl {
            public MyChildClass create() {
                return new MyChildClass(
                    first ,
                    second,
                    third ,
                    fourth
                    );
            }
        }
        ...
    }
    

    If required parameters are needed, they will look like this:

    public static class Builder {
        public double second = Double.NaN;
        public String third  = null      ;
        public MyClass create(int first) {
            return new MyClass(
                first ,
                second,
                third
                );
        }
    }
    

    Using:

    new MyClass.Builder(){{ third = "3"; }}.create(1)
    

    It is so simple that it can be used even as a builder of function parameters, for example:

    String fn = new fn(){{ first = 1; third = "3"; }}.invoke();
    

    Full github code .

    Let's move on to the difficult.

    The most sophisticated Mega Builder


    And what, in fact, can be complicated? And here is what! Let's make Builder, which in compile-time will be:
    1. do not allow the use of invalid parameter combinations
    2. do not allow building an object if required parameters are not filled
    3. prevent re-initialization of parameters

    What do we need for this? To do this, we need to create interfaces with all variants of parameter combinations, for which we first decompose the object into separate interfaces corresponding to each parameter.

    We need an interface to assign each parameter and return a new builder. It should look something like this:

    public interface TransitionNAME { T NAME(TYPE v); }
    

    At the same time, NAME should be different for each interface - because then they will need to be combined.

    You will also need getter so that we can get the value after this assignment:

    public interface GetterNAME { TYPE NAME(); }
    

    Since we need the transition-getter bundle, we define the transition interface as follows:

    public interface TransitionNAME { T NAME(TYPE v); }
    

    This will also add static control to the descriptions.

    It is approximately clear which sets of interfaces we are going to iterate over. We will now decide how to do this.

    Let's take the same class as in the previous example, 1-2-3 and write for a start all combinations of parameters. Get the familiar binary representation:

    first second third
    -     -      -
    -     -      +
    -     +      -
    -     +      +
    +     -      -
    +     -      +
    +     +      -
    +     +      +
    

    For convenience, imagine this in the form of a tree as follows:

    first second third
    -     -      -    /
    +     -      -    /+
    +     +      -    /+/+
    +     +      +    /+/+/+
    +     -      +    /+/-/+
    -     +      -    /-/+
    -     +      +    /-/+/+
    -     -      +    /-/-/+
    

    We mark the permissible combinations, for example as follows:

    first second third
    -     -      -    /       *
    +     -      -    /+      *
    +     +      -    /+/+    * 
    +     +      +    /+/+/+
    +     -      +    /+/-/+  *
    -     +      -    /-/+
    -     +      +    /-/+/+  *
    -     -      +    /-/-/+  *
    

    Let's remove the extra nodes - terminal invalid nodes and empty nodes. In the general case, this is a cyclic process that continues as long as there are nodes for deletion, but in this case we have only one terminal invalid node.

    first second third
    -     -      -    /       *
    +     -      -    /+      *
    +     +      -    /+/+    * 
    +     -      +    /+/-/+  *
    -     +      -    /-/+
    -     +      +    /-/+/+  *
    -     -      +    /-/-/+  *
    

    How to realize this?

    We need each element assignment to reduce the remaining use cases. To do this, each assignment of an element through the transition interface should return a new builder class plus a getter interface for this transition minus this transition interface.

    Draw the interfaces:

    public interface Get_first  { int    first (); }
    public interface Get_second { double second(); }
    public interface Get_third  { String third (); }
    public interface Trans_first  { T first (int    first ); }
    public interface Trans_second { T second(double second); }
    public interface Trans_third  { T third (String third ); }
    

    It’s inconvenient to draw a tablet with this; we’ll shorten the identifiers:

    public interface G_1 extends Get_first {}
    public interface G_2 extends Get_second{}
    public interface G_3 extends Get_third {}
    public interface T_1 extends Trans_first  {}
    public interface T_2 extends Trans_second {}
    public interface T_3 extends Trans_third  {}
    

    Let's draw a transition label:

    public interface B     extends T_1, T_2, T_3 {} // - - -    /       *
    public interface B_1   extends             T_2, T_3 {} // + - -    /+      *
    public interface B_1_2 extends                                    {} // + + -    /+/+    *
    public interface B_1_3 extends                                    {} // + - +    /+/-/+  *
    public interface B_2   extends T_1,             T_3 {} //          /-/+     
    public interface B_2_3 extends                                    {} // - + +    /-/+/+  *
    public interface B_3   extends T_1, T_2             {} // - - +    /-/-/+  *
    

    Define Built interface:

    public interface Built { MyClass build(); }
    

    Mark the interfaces where it is already possible to build the class with the Built interface, add getters and define the resulting Builder interface:

    //                                                  транзит
    //                                                    |                           можем строить
    //                                 геттеры            |                             |
    //                                   |                |                             |
    //                             -------------  ----------------------------------  -----
    //
    //                             first          first                                           first 
    //                             |    second    |           second                              | second
    //                             |    |    third|           |           third                   | | third
    //                             |    |    |    |           |           |                       | | |
    public interface B     extends                T_1, T_2, T_3, Built {} // - - -    /       *
    public interface B_1   extends G_1,                       T_2, T_3, Built {} // + - -    /+      *
    public interface B_1_2 extends G_1, G_2,                                          Built {} // + + -    /+/+    * 
    public interface B_1_3 extends G_1,      G_3,                                     Built {} // + - +    /+/-/+  *
    public interface B_2   extends      G_2,      T_1,             T_3        {} //          /-/+     
    public interface B_2_3 extends      G_2, G_3,                                     Built {} // - + +    /-/+/+  *
    public interface B_3   extends           G_3, T_1, T_2,             Built {} // - - +    /-/-/+  *
    public interface Builder extends B {}
    

    These descriptions are enough to enable them to build proxies in run-time, you only need to correct the resulting definitions by adding marker interfaces to them:

    public interface Built extends BuiltBase {}
    public interface Get_first  extends GetBase { int    first (); }
    public interface Get_second extends GetBase { double second(); }
    public interface Get_third  extends GetBase { String third (); }
    public interface Trans_first  extends TransBase { T first (int    first ); }
    public interface Trans_second extends TransBase { T second(double second); }
    public interface Trans_third  extends TransBase { T third (String third ); }
    

    Now we need to get the values ​​from the Builder classes to create a real class. There are two possible options - either create methods for each builder and get parameters from each builder statically-typed:

    public MyClass build(B     builder) { return new MyClass(-1             , Double.NaN      , null); }
    public MyClass build(B_1   builder) { return new MyClass(builder.first(), Double.NaN      , null); }
    public MyClass build(B_1_2 builder) { return new MyClass(builder.first(), builder.second(), null); }
    ...
    

    or use the generalized method, defined approximately as follows:

    public MyClass build(BuiltValues values) {
        return new MyClass(
            // значения из values
            );
    }
    

    But how to get the values?

    Firstly, we still have a set of builder classes that have the desired getters. Accordingly, it is necessary to check whether there is an implementation of the desired getter, and if so, cast the type to it and get the value:

    (values instanceof Get_first) ? ((Get_first) values).first() : -1
    

    Of course, you can add a method to get the value, but it will be untyped, since we cannot get the value type from existing types:

    Object getValue(final Class< ? extends GetBase> key);
    

    Using:

    (Integer) values.getValue(Get_first.class)
    

    In order to get a type, you would have to create additional classes and bundles like:

    public interface TypedGetter { Class getterClass(); };
    public static final Classed GET_FIRST = new Classed(Get_first.class);
    

    Then the method of obtaining the value could be defined as follows:

    public  T get(TypedGetter typedGetter);
    

    But we will try to get by with what are getter and transition interfaces. Then, without type casts, you can return the value only by returning the getter interface or null if such an interface is not defined for this builder:

     T get(Class key);
    

    Using:

    (null == values.get(Get_second.class)) ? Double.NaN: values.get(Get_second.class).second()
    

    That's better. But is it possible to add a default value if there is no interface while maintaining the type? Of course, it is possible to return a typed getter interface, but you still have to pass an untyped default value:

     T get(Class key, Object defaultValue);
    

    But we can use the transition interface to set the default value:

     T getDefault(Class< ? super T> key);
    

    And use it as follows:

    values.getDefault(Get_third.class).third("1").third()
    

    This is all that you can type-securely build with existing interfaces. We will create a generalized initialization method illustrating the listed use cases and initialize the resulting builder:

    protected static final Builder __builder = MegaBuilder.newBuilder(
        Builder.class, null,
        new ClassBuilder() {
            @Override public MyClass build(Object context, BuiltValues values) {
                return new MyClass(
                    (values instanceof Get_first) ? ((Get_first) values).first() : -1,
                    (null == values.get(Get_second.class)) ? Double.NaN: values.get(Get_second.class).second(),
                    values.getDefault(Get_third.class).third(null).third()
                    );
            }
        }
    );
    public static Builder builder() { return __builder; }
    

    Now you can call it:

    builder()                              .build();
    builder().first(1)                     .build();
    builder().first(1).second(2)           .build(); builder().second(2  ).first (1).build();
    builder().first(1)          .third("3").build(); builder().third ("3").first (1).build(); 
    builder()         .second(2).third("3").build(); builder().third ("3").second(2).build();
    builder()                   .third("3").build();
    

    You can download the code and see how context assist works from here .

    In particular:
    The code of the example in question: MyClass.java
    An example with generic types: MyParameterizedClass.java
    An example of a non-static builder: MyLocalClass.java .

    Total


    • Double brace initialization will not be a hack or antipater if you add a little builder.
    • It is much easier to use dynamic objects + typed access descriptors (see the TypedGetter example in the text) than to use interface assemblies or other types of statically typed objects, since this entails the need to work with reflection with all the consequences.
    • Using annotations, it would be possible to simplify the proxy generator code, but this would complicate the declarations and probably worsen the detection of inconsistencies in compile-time.
    • And finally, in this article we finally and irrevocably determined the minimum and maximum complexity limits of the Builder pattern - all other options are somewhere between them.

    Also popular now: