StringTemplate Part 4:Generating a Builder Class using StringTemplate
Join the DZone community and get the full member experience.
Join For FreeIn “Effective Java: Second Edition” (go read it, no really go read it) Mr. Block puts forth the idea of using a “Builder” class to help construct complicated objects. His builder uses a “fluid interface” that also has the extra side effect of looking like named parameters.
Here is how you might use a builder class to create Planet objects.
Planet mercury = new PlanetBuilder().name("Mercury").mass(0.06f).build();The only issue I have with this type of API is that it requires a lot of maintenance. Every time a property is added to the target class another method must be added to the builder and that data needs to be factored into the build method. I believe that with StringTemplate and a dash of reflection we can generate a builder for any (Bean) class. Lets take a look at the Planet class to see what we are up against.
System.out.println(mercury);
Planet mars = new PlanetBuilder().name("mars").diameter(0.532f)
.mass(0.11f).orbitalRadius(1.52f).orbitalPeriod(1.88f)
.inclinationToSunsEquator(5.65f).orbitalEccentricity(0.093f)
.rotationPeriod(1.03f).moons("Phobos","Deimos")
.rings(false).atmosphere("CO2,N2").build();
System.out.println(mars);
public class Planet {
private String name;
private float diameter;
private float mass;
private float orbitalRadius;
private float orbitalPeriod;
private float inclinationToSunsEquator;
private float orbitalEccentricity;
private float rotationPeriod;
private String[] moons;
private boolean rings;
private String atmosphere;
private List<String> notes;
...getters and setters ...
}
As you can see we have 12 private variables. In this case every private variable maps to a “property” with a getter and setter pair (not shown). Since this class implements getter and setters for it's properties we can use the java.beans.Introspector class to construct “property descriptors” that can give us all the information we need to generate a builder object.
But what information do we need?
- The package this generated class belongs to.
- Name for the builder class.
- The name of each property
- The type of each property.
- The setter method for each property.
- Package Name:
- The class needs to belong to some package. We can't really infer this from anywhere so it becomes a top level parameter to the template.
- Builder Class Name:
- In Effective Java a public static inner class called “Builder” is nested inside the target class. In this case we won't (or can't) modify the original source file. I have chosen simply to append “Builder” to the target classes name. In this case Planet → PlanetBuilder. The refection class “Class” has a getName() method which can provide us with the target classes name.
- Property Names:
- The PropertyDescriptor class also has a getName() method. This is the name part of a getter/setter pair. For example getMass + setMass define a property called “mass”. Each property has a unique name and we depend on this fact in our templates.
- Property Types:
- One would think that the Type of a property would be as easy to determine as the Name. Unfortunately primitives, arrays and generics make this slightly more complicated, yet not impossible. If you are interested in the details please see the source code at the end of this article. Lets move on assuming the Type is easily accessible.
- Property “setter”:
- Setters in reflection speak translates into the write method. Property descriptor has getWriteMethod() which returns a Method object that represents the method of the target class. We will use this to generate the build logic
The base template:
I've chosen put all these templates in one template group file (BuilderGenerator.stg). We start with the main class template.
main(class, properties, packageName) ::= <<
package $packageName$;
import $class.name$;
public class $class.simpleName$Builder {
}
>>
There are three parameters. The target class, a list of properties and the package name.
contentTemplate.setAttribute("class", Planet.class);
contentTemplate.setAttribute("properties", wrapProperties(Planet.class));
contentTemplate.setAttribute("packageName",“java.net.string.template.demo”);
These three parameters correspond to what gets pushed into the template. We can generate all our code from just these three parameters. (OK, that not totally true. I had to do some wrapping of PropertyDescriptor to make things easier. See the source code at the end of this article for details.)
If we ran the template now against Planet.class we would have an empty class named PlanetBuilder.
Builder member variables:
Next we add the template for the private variables.
builderVar(property) ::= <<
private $property.type$ $property.name$;
>>
We need to store our builder arguments somewhere so our build() method can use them later. For each variable we need a type and a name. Since each property has a unique name it's safe to use the property name as private variable name. Now we take the builderVar() template and apply it to our list of properties in the main template.
public class $class.simpleName$Builder {
$properties:builderVar()$
...
Running the template now will generate a builder class will all of it's private members declared.
"IsSet" variables:
In Effective Java a builder has defaults specified for all of it's internal variables. Since we are are generating a builder for any class we can't possibly know the proper default value for any particular property. To get around this we need to keep track of which builder methods were called. Here I have split up the template for the name of the variable from it's actual declaration. It will allow me to use isSetVarName later from the build method.
/**
* Tracks if the $property.name$ property has been set on this builder
*/
private boolean $isSetVarName(property)$ = false;$\n$
>>
/**
* Generates the boolean "isSet" variable names
*/
isSetVarName(property) ::= <<$property.name$IsSet>>
Once again we must remember to add this template to the main template.
public class $class.simpleName$Builder {
$properties:builderVar()$
$properties:isSetVarDec()$
…
Builder Methods:
This is the public facing API for the builder. We need a name, a parameter type, a parameter name and a return type. The return type is always the builder class. This is what makes fluid interfaces possible. I've used the property name as the builder method name. It takes one parameter of the same type as the property. The “is Set” boolean is set to true when this method is called indicating that this value should be used when constructing a new target object.
public $class.simpleName$Builder $property.name$($property.type$ $property.name$) {
this.$property.name$ = $property.name$;
$isSetVarName(property)$ = true;
return this;
}
Here is what the mass method looks like from Planet.
public PlanetBuilder mass(float mass) {
this.mass = mass;
massIsSet = true;
return this;
}
Just like the other template this one also needs to be added to the main template.
Object Construction:
public $class.simpleName$ build() {
$class.simpleName$ bean = new $class.simpleName$();
$properties:apply()$
return bean;
}
This is the final method of our template. Most of this should be familiar by now. The big change is that instead of generating lots of methods or variable declarations we generate blocks of code using the apply template.
apply(property) ::= <<
if($isSetVarName(property)$){
bean.$property.writeMethod.name$($property.name$);
}
>>
In this template we start off re-using the isSetVar template to generate the name of the boolean that controls this property. Then we walk the object graph from the property → getWriteMethod → getName() for that actual method name. Since this is a bean property we can assume the parameter count is one and just pass in the builder variable.
Here is what the apply block looks like from the planet class.
if(massIsSet){
bean.setMass(mass);
}
Generating builder classes:
This completes our template. Running the final product on Planet.class now will generate this builder class. We can also point our generator at other beans to generate builders. Here are some examples I've taken from Swing (Swing components are all beans).
JFrameBuilder
JTableBuilder
JComboBoxBuilder
GridLayoutBuilder
Conclution:
I think StringTemplate really made this code generation significantly less complicated. The code generation core is one class, one template file and one extra model object to smooth out the refection api interaction. Integrating something like this into a build process could help generate more user friendly API's.
Now that we have basic functionality, we can start to add more fancy constructs. That though, must wait for another day.
Opinions expressed by DZone contributors are their own.
Comments