March 22, 2015

Groovy by example: Writing a builder (part 1 of 2)



This article contains an in-depth study of Groovy’s BuilderSupport class and a tutorial to create custom builder classes as well as some associated best-practices I found useful. Note: The example source code is available on GitHub.

Groovy has native syntactic support for builder structures – which really takes the builder design pattern to the next level. With its nested syntactic elements, a builder is perfect to describe any hierarchically built structure. For some typical use cases of structured models, Groovy provides out-of-the-box builder implementations. Examples include:
Apart from that, Groovy also offers a means to create your own builder. If you have your own structured business model, a builder might offer the perfect API to easily and concisely build instances of your model, e.g. in UnitTests. The only precondition is that your model is structured in a “1 parent to 1-n children” hierarchy.

In order to implement our own builder, we have to extend the BuilderSupport class. We simply have to implement some “business” methods of that class, and it will behave like a true structure builder at runtime. In this article, I will concentrate on how to design you own BuilderSupport sub-class with some stress on BuilderSupport’s internal mechanisms. This will allow us to take the Groovy builder syntax even further; we will also use some meta-programming to enhance our implementation.

The goal

For a simple use case let’s assume we have a model which represents a household, accompanied with insurance information:

  • a household has one or more rooms, and one or more cars associated with it;
  • a room has one or more pieces of furniture.
If we’d like to instantiate an example Household object, e.g. in a UnitTest, we would have to write a ridiculous amount of code in plain old Java; something like:
protected Household createDemoHousehold() {
    Household household = new Household("main street 42");
    household.getCars().add(new Car("VW", 15000));
    Room livingRoom = new Room("living room");
    livingRoom.getFurniture().add(new Furniture("TV", 2000));
    livingRoom.getFurniture().add(new Furniture("sofa", 1000));
    household.getRooms().add(livingRoom);
    return household;
}

Just for a household with a car and two pieces of furniture! This code is very hard to read. Luckily, with Groovy’s basic syntax, we can already lighten things a bit:
protected Household createDemoHousehold() {
    Household household = new Household("main street 42")
    household << new Car("VW", 15000)
    Room livingRoom = new Room("living room")
    livingRoom.furniture << new Furniture("TV", 2000)
    livingRoom.furniture << new Furniture("sofa", 1000)
    household.rooms << livingRoom
    return household
}

Now at least, the getter / setter chaos is gone, but this code still is very procedural whereas we’d like to describe a model in a declarative way, just as we would using some structured document format.

Here is what we’d like to end up with:
HouseholdBuilderPlus.build {
    household(address: "main street 42") {
        car(VW, price: 15000)
        room {
            name(livingRoom)
            furniture(name: TV, price: 2000)
            furniture(name: sofa, price: 1000)
        }
    }
}

We can do this in about a hundred lines of code with Groovy’s BuilderSupport.

The basics of BuilderSupport

First of all let’s think about how Groovy builders work. Obviously, there’s no magic involved. If we examine above builder structure we see that when thinking in Groovy syntax, it could be translated into nested method calls where the closure parameter of a method can contain other method calls. Thus we could actually implement a builder on our own by just handling this kind of closure nesting.

But we don’t want to re-invent the wheel here, thus we’ll use the BuilderSupport base class which cares about the handling of nested structures. We only need to provide two instructions by implementing two methods:
  • createNode(…): How to translate a builder method call to a node constructor invocation (this method comes in four overloaded flavors);
  • setParent(parent, child): How to associate a newly-created node to its surrounding parent node.

To understand this better, let’s see BuilderSupport’s behavior in action by implementing a simple demo builder:

class DemoBuilder extends BuilderSupport {
    @Override
    public void setParent(Object parent, Object child) {
        println "parent $parent << child $child"
    }
    
    @Override
    public Object createNode(Object name) {
       return createNode(name, null, null);
    }

    @Override
    public Object createNode(Object name, Object value) {
       return createNode(name, null, value);
    }

    @Override
    public Object createNode(Object name, Map attrs) {
       return createNode(name, attrs, null);
    }

    @Override
    public Object createNode(Object name, Map attrs, Object value) {
        println "* node $name with args $attrs and value $value."
        return name
    }
}

If we invoke it like so:
new DemoBuilder().build {
    household(adress: "main street 42") {
        car(name: "VW", price: 15000)
        room {
            name("living room")
            furniture(name: "TV", price: 2000)
            furniture(name: "sofa", price: 1000)
        }
    }
}
…it will print out this result:
* node build with args null and value null.
* node household with args [adress:main street 42] and value null.
parent build << child household
* node car with args [name:VW, price:15000] and value null.
parent household << child car
* node room with args null and value null.
parent household << child room
* node name with args null and value living room.
parent room << child name
* node furniture with args [name:TV, price:2000] and value null.
parent room << child furniture
* node furniture with args [name:sofa, price:1000] and value null.
parent room << child furniture

Now what is actually going on here?

In order to grasp the mechanisms of BuilderSupport it’s crucial to understand that the implementation is stateful. The current property always holds the most recently created node. This also explains why we always need to create a new builder instance; builder methods can’t be static.

Thus when we invoke a simple method on the builder, say name("living room"), it is essentially translated to:
def parent = current
current = createNode("name", "living room")
setParent(parent, current) 

Note that depending on the signature of the method we invoked, the corresponding createNode(…) flavor will be chosen:
method() => createNode(method)
method(value) => createNode(method, value)
method(Map args) => createNode(method, args)
method(value, Map args) => createNode(method, args, value)

The return value of the respective method is then assigned to the builder’s current property, as shown above.

Note that the closure object at the end of each method call is of course not handed to the respective createNode(…) call: Instead, it is evaluated, thus serving as the nesting mechanism for inner structures.

Whatever createNode(…) call is chosen, the newly-created node will then immediately be invoked alongside the former “current” node in the setParent(…) method. This method has no return value. It is just responsible for creating the “child” link between the formerly-created and the newly-created node. This is why you can literally build a builder for any structure with a well-defined parent-child relationship!

More about BuilderSupport

There are a few things we can find out when observing the various createNode(…) signatures:

First of all, the name of the method we invoke in the builder is part of the information sent to the createNode(…) handler. This means that we can truly react to dynamic method calls.

Most importantly, we find that the range of method call signatures supported by the builder is actually strictly limited to the four signatures listed above. What if we use a non-compliant method signature such as this...?
method(value1, value2) => createNode(???)

I’m tempted to try this out:
new DemoBuilder().build {
    household(adress: "main street 42") {
        car("VW", 15000)
    }
}

As expected, we run into an exception:
groovy.lang.MissingMethodException: No signature of method: 
ch.codebulb.groovybyexample.buildersupport.DemoBuilderTest.car() 
is applicable for argument types: (java.lang.String, java.lang.Integer) values: [VW, 15000]

This is very good because it means that we can enhance our builder with whatever additional methods we’d like to support. For example, we can add:
public Object car(String name, int price) {
    return createNode("car", [name: name, price: price]);
}

To make the example work again. We could thus also make use of meta-programming to allow arbitrary method calls at runtime.

Pages: 1 2

No comments: