Groovy for Domain-Specific Languages

October 8, 2010

Groovy

«»

FactoryBuilderSupport


BuilderSupport is the base class for many of the builder classes provided in the
Groovy packages. As we can see from the previous examples, it is easy to work with.
We have built quite a useful database builder tool in relatively few lines of code.


However, one issue with BuilderSupport is that the hook functions are in effect
funnels for handling all of the possible tags that we might like to process in our
markup. In our CustomerBuilder we are handling just four different tags.


This is not a realistic scenario for most database schemas. We could expect to have
dozens more tag types that we need to handle if we wanted to expand this example
into something that would work with a typical database schema for even a modestly
sized application. Funneling all of these tags into one createNode would create an
unwieldy mess of code.


def createNode(name){
Object result = null
switch (name) {
case “customer”:
return new Customer(firstName:”", lastName:”")
case “invoice”:
return new Invoice()
case “sales_order”:
return new SalesOrder(sku:”default”,amount:1,price:0.0)
case “another_object”:
return new Something()
…….. and more!
}
}


Groovy provides a second builder support class that neatly overcomes this problem.
The Groovy.util.FactoryBuilderSupport class is based on the factory pattern,
and delegates the handling of individual tag objects to Factory classes. Originally,
this support class was just provided as part of the SwingBuilder. Because it
was clear that this was more generally useful, the code was then refactored to be
generally usable as a standalone Builder class. Since then, it has become the basis
for other builders, such as the recently-added JmxBuilder, and is available to us for
deriving our own factory-based builders.


FactoryBuilderSupport works by orchestrating the construction process in concert
with Factory classes. When the FactoryBuilderSupport class encounters a method
tag, it constructs an appropriate Factory object to handle it. The factory provides
method hooks that implement the construction of the individual object and the
setting up of parent-child relationships between the objects.


To implement a builder with the FactoryBuilderSupport class, we must first
declare a Factory class for each object type that we wish to process. Factory classes
are derived from the Groovy.util.AbstractFactory class and need to overload
some or all of the following methods from the AbstractFactory class:



  • newInstance

  • Called by FactoryBuilderSupport whenever it wants an object of a particular
    type to be constructed. It is similar to the createNode methods of
    BuilderSupport except that there is just one newInstance method, which
    accepts all argument types regardless of whether a value or attributes are
    supplied or not.


  • onHandleNodeAttributes

  • Allows the Factory class to take over the management of attributes. It can
    stop the builder from processing attributes by returning true.


  • setParent and setChild

  • Provide hooks for managing the parent-child relationships between objects.


  • isLeaf

  • We set this method to return true if the method tag being handled should
    be a leaf node and stops the builder treating any subsequent method calls
    as object declarations.


  • onNodeCompleted

  • Called when a node is completed, in order to allow any finalization of the
    object to be performed. It is similar to nodeCompleted in BuilderSupport.



To build a replacement for the CustomerBuilder with FactoryBuilderSupport,
we first need to define Factory classes for each of the tag methods that we need to
process. The first of these is the customers tag, which is straightforward enough.
This tag does not cause any objects to be created, so all we do is return the tag name
as the object created.



public class CustomersFactory extends AbstractFactory {
public boolean isLeaf() {
return false
}
public Object newInstance(FactoryBuilderSupport builder,
Object name, Object value, Map attributes
) throws InstantiationException, IllegalAccessException {
return name
}
}


We then define a factory class for the customer object. The methods that we need to
implement are isLeaf (returns false), newInstance (to create the customer object),
and onNodeCompleted (to save it).



public class CustomerFactory extends AbstractFactory {
public boolean isLeaf() {
return false
}
public Object newInstance(FactoryBuilderSupport builder,
Object name, Object value, Map attributes
) throws InstantiationException, IllegalAccessException {
Customer customer = null
if (attributes != null)
customer = new Customer(attributes)
else
customer = new Customer()
return customer
}
public void onNodeCompleted(FactoryBuilderSupport builder,
Object parent, Object customer) {
customer.save()
}
}


The factory for invoices is equally straightforward. The only addition is that we
need to take care of the parent-child relationship between customer and invoice.
We do this by adding a setParent method, which will call addToInvoices on the
customer object if required. We also need to check the value parameter passed to
newInstance to see if a parent is being set at this point.



public class InvoiceFactory extends AbstractFactory {
public boolean isLeaf() {
return false
}
public Object newInstance(FactoryBuilderSupport builder,
Object name, Object value, Map attributes
) throws InstantiationException, IllegalAccessException {
Invoice invoice = null
if (attributes != null)
invoice = new Invoice(attributes)
else
invoice = new Invoice()
if (value != null && value instanceof Customer)
value.addToInvoices(invoice)
return invoice
}
public void setParent(FactoryBuilderSupport builder,
Object parent, Object invoice) {
if (parent != null && parent instanceof Customer)
parent.addToInvoices(invoice)
}
public void onNodeCompleted(FactoryBuilderSupport builder,
Object parent, Object invoice) {
invoice.save()
}
}


The factory for sales orders is identical to invoices except that we now return true
from isLeaf because a sales order object will always be a leaf node in our tree.



public class SalesOrderFactory extends AbstractFactory {
public boolean isLeaf() {
return true
}
public Object newInstance(FactoryBuilderSupport builder,
Object name, Object value, Map attributes
) throws InstantiationException, IllegalAccessException {
SalesOrder sales_order = null
if (attributes != null)
sales_order = new SalesOrder(attributes)
else
sales_order = new SalesOrder()
if (value != null && value instanceof Invoice)
value.addToOrders(sales_order)
return sales_order
}
public void setParent(FactoryBuilderSupport builder,
Object parent, Object sales_order) {
if (parent != null && parent instanceof Invoice)
parent.addToOrders(sales_order)
}
public void onNodeCompleted(FactoryBuilderSupport builder,
Object parent, Object sales_order) {
sales_order.save()
}
}


All of the intelligence of how to orchestrate the construction process is encapsulated
in the FactoryBuilderSupport class. So literally all we need to do for the whole
builder to work is to register the Factory classes with appropriate tag names.



public class CustomersFactoryBuilder extends FactoryBuilderSupport {
public CustomersFactoryBuilder(boolean init = true) {
super(init)
}
def registerObjectFactories() {
registerFactory(“customers”, new CustomersFactory())
registerFactory(“customer”, new CustomerFactory())
registerFactory(“invoice”, new InvoiceFactory())
registerFactory(“sales_order”, new SalesOrderFactory())
}
}


FactoryBuilderSupport uses refl ection at runtime to detect what registration
methods to run. By scanning the list of methods in the MetaClass instance and
looking for methods that begin with “register”, FactoryBuilderSupport
detects whether any additional registration methods are provided in the derived
builder class. In the preceding example, the only registration method added is
registerObjectFactories, but we could well have written:



def registerCustomers() {
registerFactory(“customers”, new CustomersFactory())
}
def registerCustomer() {
registerFactory(“customer”, new CustomerFactory())
}
def registerInvoice() {
registerFactory(“invoice”, new InvoiceFactory())
}
def registerSalesOrder() {
registerFactory(“sales_order”, new SalesOrderFactory())
}


FactoryBuilderSupport would detect all of these and run them in turn. Which
method you use is a matter of choice. The only issue that you need to be aware of is
that the registration methods will not be called in any predetermined order. If there
are dependencies in your registration code, then it’s best to group these into a single
registration method.


To finish, we can drop this modified builder right where we previously used
CustomersBuilder, and it will work in the same way.



def builder = new CustomersFactoryBuilder()
def customers = builder.customers {
fred = customer(firstName:”Fred”,lastName:”Flintstone”) {
invoice {
salesOrder(sku:”productid01″, amount:1, price:1.00)
salesOrder(sku:”productid02″, amount:2, price:1.00)
salesOrder(sku:”productid03″, amount:3, price:1.00)
}
}
invoice2 = invoice(fred)
salesOrder(invoice2, sku:”productid04″, amount:1, price:1.00)
salesOrder(invoice2, sku:”productid05″, amount:1, price:1.00)
salesOrder(invoice2, sku:”productid06″, amount:1, price:1.00)
}


In terms of management and maintenance, this version is far superior. Adding
capabilities now for new tables will simply involve writing a new Factory class
and registering it.


Summary


In Chapter 5, we discussed how builders worked via the Groovy MOP. In this
chapter, we have taken a deeper look at how features of the MOP are used to
implement the builder pattern. We’ve looked at the language features used to
create a builder, and seen how they involve implementing pretended methods and
infl uencing how methods calls are resolved. Implementing a builder directly by
using the MOP in this way focuses on the nuts and bolts of the semantics of the
builder, rather than the construction process.


In this chapter, we have seen how Groovy provides two useful support classes that
make it much simpler to implement our own builders than if we use the MOP. We’ve
seen how to use BuilderSupport and FactoryBuilderSupport to create our own
builder classes.


Using these support classes greatly simplifies the implementation of builders.
Hopefully, this will inspire you to see opportunities to develop your own Groovybased
builders for you own applications. You can find the full documentation for all
of the classes that we covered here on the Codehaus website. The Groovy document
for the classes can be found at http://Groovy.codehaus.org/api/Groovy/util/
package-summary.html.

Groovy Articles

email

«»

Comments

comments