March 22, 2015

Groovy by example: Writing a builder (part 2 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. Make sure to check out part 1 first.

Implementing BuilderSupport

With what we know about BuilderSupport now, we can implement a basic builder for our household model:
class HouseholdBuilder extends BuilderSupport {
    private static final String MODEL_PACKAGE = "ch.codebulb.groovybyexample.buildersupport.model"
    
    private static final Map SUPPORTED_MODELS = [
        build: ArrayList,
    ].withDefault {
        Class.forName("${MODEL_PACKAGE}.${it.capitalize()}")
    }
    
    @Override
    public void setParent(Object parent, Object child) {
        if (parent == child) return
        parent << 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) {
        try {
            def instance
            if (!value) {
                instance = SUPPORTED_MODELS[name].newInstance()
            }
            else {
                instance = SUPPORTED_MODELS[name].newInstance(value)
            }
            attrs.each {k, v -> instance."$k" = v}
            return instance
        }
        catch (ClassNotFoundException ex) {
            if (value && !attrs) {
                current."$name" = value
                return current
            }
            else {
                throw ex
            }
        }
    }
}

We can then invoke and test it like so:
assert [createDemoHousehold()] == new HouseholdBuilder().build {
    household(address: "main street 42") {
        car("VW", price: 15000)
        room {
            name("living room")
            furniture(name: "TV", price: 2000)
            furniture(name: "sofa", price: 1000)
        }
    }
}

In the builder implementation, I used a reflection-based dynamic solution to retrieve the class we want to create a node instance of, which I will presently explain.

Note that this solution enables the model classes to be pure POJOs; the “container” models must provide a leftShift(…) method, but that’s all.

createNode(…)

As in the example code, you will typically implement only one of the createNode(…) methods and implement the others as pure delegates.

In the actual implementation, you will implement one or both of these use cases:
  • The builder method call should add a new node, as in furniture(name: "TV", price: 2000);
  • The builder method call should set a property of the current node, as in name("living room").
Hence, you will typically implement createNode(…) as a giant switch-case where the name argument is the switch candidate: Based on the name, you will then either create a new node of the appropriate class, or call the appropriate setter on the current node.

I implemented a dynamic solution here to serve both cases without having to specify the individual case branches. This will allow to use new or updated model classes immediately, without changes to the builder (open/closed principle compliant).

First of all, I specified all SUPPORTED_MODELS classes to be either an ArrayList or one of the classes in my model package. I associated the ArrayList implementation explicitly with the build builder call (I will explain this in the “best practices” section below), and through the withDefault(…) GDK method, I let the builder choose a model class at runtime based on the method name for any other builder calls.

In createNode(…) I also distinguish whether a value argument has been provided or not. If so, we assume this argument to be the one constructor call argument. The attrs arguments are used to set the properties of the newly-created node instance, as if it were a Map constructor call.

Finally I have to cover the second use case where createNode(…) should not create a new instance, but just update a property on current. Here, I can detect this case by finding that no class for the given node name has been found. Then, I use the value argument to set the property in question.

Note that in this latter case, I access and modify the current property directly. This should be considered a hack. Because BuilderSupport updates the current property automatically, we would normally not access this property ourselves. But this technique comes in handy if we want to override the BuilderSupport default – that any createNode(…) call would always result in the creation of a new property node. Note also that in this special case, I return the altered current node.

setParent(…)

The setParent(…) method should really not be more than a mere delegate to either the child setting its parent, or the parent setting its child. If you use a swich/case here, you violate the open/closed principle. Note that your model classes must thus provide a common (implicit) interface; in this example, it’s the leftShift(…) method.

Because of how I implemented the use case of createNode(…) actually altering a property of the current node instead of creating a new node, I must here handle the special case that the parent node is again returned as the child node. In this case, of course, I don’t do anything.

Best practices and additional tricks

Use a containing collection

In most cases I found it useful to use the “outermost” builder method to build a List wrapper around whatever model is built by the builder. That’s why in the example, build() returns a List. This makes it easy to use the builder for building an arbitrary number of models. Otherwise you would have to invoke the builder multiple times. So you can now do:
new HouseholdBuilder().build {
    household(address: "main street 42")
    household(address: "main street 44")
}

Without that containing collection, you would have to write:
new HouseholdBuilder().household(address: "main street 42")
new HouseholdBuilder().household(address: "main street 44")

Because the household(…) method returns the node instance it created, you cannot chain another household(…) method – it would be interpreted as a method call on the Household object returned.

Use a static factory method

We can use a static factory method as a shorthand for instantiating a new builder instance, like so:
HouseholdBuilderPlus.build { household(address: "main street 42") }
A static method similar to this one will do the trick:
public static List build(Closure closure) {
    return new HouseholdBuilderPlus().invokeMethod("build", closure)
}

Note that we used the dynamic invokeMethod(…) call here only because the static and non-static “build” methods have signature collisions, and a call to the method would simply end in a StackOverflowError. This is not required if you name your static method different than any builder method.

Use convenience methods

A method invocation to a builder typically gets delegated to the appropriate createNode(…) method. However, we can always override this behavior by simply adding additional methods to our builder class. This can come in handy, e.g. to provide “templates” for frequently used structures:
HouseholdBuilderPlus.build {
    household(address: "main street 42") {
        livingRoom {
            furniture(name: "TV2", price: 2000)
        }
    }
}

Here, for example, we use a “living room” template we defined as a method in our builder:
public livingRoom(String roomName="living room", Closure closure) {
    room {
        name(roomName)
        furniture(name: "TV", price: 2000)
        furniture(name: "sofa", price: 1000)
        closure?.call()
    }
}

Note that we can even offer a customizable template e.g. by accepting method arguments or by accepting and invoking a closure parameter, just as we did here.

Use Groovy code

Before defining tons of convenience methods, keep in mind that your builder DSL still is 100% valid Groovy code. This means that the caller can use arbitrary Groovy code structures, such as variable access, conditionals and loops inside the builder syntax. Clever use of these possibilities should reduce the need for templates. For example:
HouseholdBuilderPlus.build {
    household(address: "main street 42") {
        2. times { car("VW", price: 15000) }
    }
}

Use Auto-to-String

A convenient addition to our builder DSL would be that we don’t need to enclose Strings in opening and closing quotes. Using some meta-programming, this is actually quite easy. If the caller would just omit the quotes without further actions, Groovy would throw an exception because it would treat the String as the name of a property of the builder which it cannot resolve. Luckily, we have the propertyMissing(String) method which can override this behavior:
public String propertyMissing(String name) {
    // replace camelCase with "whitespace separated"
    name.replaceAll (/[A-Z](?=[a-z])/) {" ${it.toLowerCase()}"}
}

Now we can omit quotes for simple Strings (e.g. “living room”, “TV”, etc.):
HouseholdBuilderPlus.build {
    household(address: "main street 42") {
        car(VW, price: 15000)
        room {
            name(livingRoom)
            furniture(name: TV, price: 2000)
            furniture(name: sofa, price: 1000)
        }
    }
}

You should consider this option only if your builder DSL makes use of many single Strings. You should disable it while you debug your builder.

Use other dynamic features

By combining the power of the BuilderSupport implementation and Groovy’s meta-programming capabilities, you can build a truly dynamic DSL for your builder, just as we discussed it above for some example applications.

Using ObjectGraphBuilder

With ObjectGraphBuilder, Groovy features a similar highly dynamic builder to build object structures as we’ve implemented from scratch in the Implementing BuilderSupport section.

Building a simple structure of arbitrarily structured POJOs with ObjectGraphBuilder is as simple as:
new ObjectGraphBuilder(
    classLoader: this.class.classLoader,
    classNameResolver: "ch.codebulb.groovybyexample.buildersupport.model",
).household(address: "main street 42") {
    car(name: "VW", price: 15000)
    room(name: "living room") {
        furniture(name: "TV", price: 2000)
        furniture(name: "sofa", price: 1000)
    }
}
Note however that this builder doesn’t support “building” properties nor any other to the previously discussed more sophisticated techniques. However, you can of course base your own Builder implementation on ObjectGraphBuilder rather than on plain BuilderSupport and then build the previously discussed additional features as you need them! Also note that ObjectGraphBuilder itself comes with various options to customize builder behavior.

Conclusion

The internals of BuilderSupport are not obvious. As it’s one of Groovy’s most powerful feature for DSL creation, it’s worth a closer look. Once you grasped its concept, implementing basic custom builders with BuilderSupport is really straightforward whilst the tool will scale with your requirements thanks to its highly dynamic nature.

I hope this article will help you kick-start your own builder implementation. Feel free to check out the example source code at GitHub. Please let me know in the comments section below whether you found this article useful, and bring in any improvement suggestions.

Updated March 13, 2016: “Using ObjectGraphBuilder” section added.


Pages: 1 2

No comments:

Post a Comment