⬆️ ⬇️

The easiest and most complex Java builder





One of the frequently considered patterns is the Pattern Builder. Basically, the 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 clear as a stool, but some understatement is felt - then the minimal variant is declared anti-pattern, then more complex cases are ignored. I would like to correct this point by examining the limiting cases and determining the minimum and maximum limits of the complexity of this pattern.

')

So, consider them:



Minimal builder or Rehab double brace initialization



First, consider the minimal builder that people often forget about - 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 standard equals is 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 with the inherited class, equals will return false. But we create an anonymous inherited class and interfere with the chain of inheritance.

  2. Possible memory leak, as an anonymous class will hold a link to the creation context.
  3. Initialization of fields without checks.


In addition, it is thus impossible to create immutable objects, since you cannot use final fields.



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



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


It uses methods, not direct access to the fields, and equals compatibility is usually not required. So how can we use such an unreliable hack-like method? It's very simple - by selecting a separate builder class for double brace initialization.



The code of such a builder contains only definitions of fields with default values ​​and construction methods that are responsible for checking parameters and invoking 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 in the chain of inheritance - it is a separate class.
  2. Builder does not flow - its use is terminated after the creation of the object.
  3. Builder can control the parameters in the object creation method.


Voila! Double brace initialization rehabilitated.



To use inheritance, Builder is divided into two parts (one with fields, the other with the method of creation) 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 required, 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 you can use it at least as a function parameter builder, for example:



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


Full code on github .



Let us turn to the difficult.



The most complex Mega Builder



And what, in fact, can be complicated? And that's what! Let's create a Builder, which in compile-time will be:

  1. prevent the use of invalid combinations of parameters
  2. do not allow building the object if the required parameters are not filled
  3. prevent reinitialization 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 do the decomposition of the object into separate interfaces corresponding to each parameter.



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



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


In this case, NAME should be different for each interface - after all, they will then need to be combined.



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



 public interface GetterNAME { TYPE NAME(); } 


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



 public interface TransitionNAME<T extends GetterNAME> { T NAME(TYPE v); } 


This will also add static control in the descriptions.



It's pretty clear what sets of interfaces we are going to sort through. Now we decide how to do it.



Take the same class as in the previous 1-2-3 class and write out all the combinations of parameters for a start. Get the familiar binary representation:



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


For convenience, we will present this in the form of a tree as follows:



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


Let's mark acceptable combinations, for example:



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


Remove unnecessary nodes - terminal invalid nodes and empty nodes. In the general case, this is a cyclic process that continues until there are nodes to delete, but in this case we have only one terminal invalid node.



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


How to implement it?



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



Draw interfaces:



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


The label with this draw is inconvenient, we reduce 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<T extends G_1> extends Trans_first <T> {} public interface T_2<T extends G_2> extends Trans_second<T> {} public interface T_3<T extends G_3> extends Trans_third <T> {} 


Draw a label of transitions:



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


Define Built Interface:



 public interface Built { MyClass build(); } 


Mark the interfaces where it is already possible to build a 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<B_1 >, T_2<B_2 >, T_3<B_3 >, Built {} // - - - / * public interface B_1 extends G_1, T_2<B_1_2>, T_3<B_1_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<B_1_2>, T_3<B_2_3> {} // /-/+ public interface B_2_3 extends G_2, G_3, Built {} // - + + /-/+/+ * public interface B_3 extends G_3, T_1<B_1_3>, T_2<B_2_3>, Built {} // - - + /-/-/+ * public interface Builder extends B {} 


These descriptions are enough for them to be able to build a proxy at run-time, you just need to correct the resulting definitions by adding to them marker interfaces:



 public interface Built extends BuiltBase<MyClass> {} 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 <T extends Get_first > extends TransBase { T first (int first ); } public interface Trans_second<T extends Get_second> extends TransBase { T second(double second); } public interface Trans_third <T extends Get_third > extends TransBase { T third (String third ); } 


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



 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?



First of all, we still have a set of builder classes that have the necessary getters. Accordingly, it is necessary to check if there is an implementation of the desired getter and, if there is, to 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 type of the value from the existing types:



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


Using:



 (Integer) values.getValue(Get_first.class) 


In order to get the type, I would have to create additional classes and bundles like:



 public interface TypedGetter<T, GETTER> { Class<GETTER> getterClass(); }; public static final Classed<T> GET_FIRST = new Classed<Integer>(Get_first.class); 


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



 public <T, GETTER> T get(TypedGetter<T, GETTER> typedGetter); 


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



 <T extends GetBase> T get(Class<T> 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, saving the type? Of course, it is possible to return a typed getter interface, but you still have to pass an untyped default value:



 <T extends GetBase> T get(Class<T> key, Object defaultValue); 


But we can use the default transition-interface for setting the values:



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


And use this as follows:



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


This is all that can be type-built with existing interfaces. Create a generic initialization method illustrating the listed use cases and initialize the resulting builder:



 protected static final Builder __builder = MegaBuilder.newBuilder( Builder.class, null, new ClassBuilder<Object, MyClass>() { @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(); 


Download the code and see the work context assist from here .



In particular:

Code of the considered example: MyClass.java

Example with generic types: MyParameterizedClass.java

An example of a non-static builder: MyLocalClass.java .



Total



Source: https://habr.com/ru/post/261163/



All Articles