Groovy for Domain-Specific Languages

October 8, 2010

Groovy

«»

BuilderSupport


Under the hood, all of Groovy‘s own Builders are implemented by using the
invokeMethod or methodMissing and delegate techniques that we have described
above. We could choose to start creating our own builder classes by using these
features alone. Perhaps the biggest challenge when creating a builder with these
features alone is that the MOP concepts of pretended methods and delegate handling
don’t fit well with the task at hand—namely the construction of complex objects. It
would be nice to have APIs that refl ected the task at hand in a better way.


Thankfully, the complexities of managing invokeMethod or methodMissing calls
and figuring out who the delegate should be are encapsulated into the builder
support classes provided in the Groovy packages. The most basic support class
is Groovy.util.BuilderSupport.

BuilderSupport hook methods


BuilderSupport provides an interface to the building process that nicely mirrors
the node-based construction process on which most builder classes are based.
Instead of overriding invokeMethod as in the initial example, we override the
node construction methods provided by BuilderSupport.


These methods provide the hooks such that instead of coding at the MOP level with
pretended method invocations and delegates, we can code with respect to the node
construction process. The important hook methods that we need to know about are
listed here. These methods encapsulate the building process out into node creation
style events that make far more sense from an object construction point of view. Then
we never need to worry about pretended methods or delegates again.



  • createNode(Object name)

  • createNode(Object name, Object value)

  • createNode(Object name, Map attributes)

  • createNode(Object name, Map attributes, Objects value)

    Called by BuilderSupport whenever a pretended method is encountered.
    The name parameter contains the name of the pretended call. The
    responsibility of the hook is to return a new object of a type appropriate to
    the method call. The specific hook to be called depends on what parameters
    are passed to the method.

  • nodeCompleted(Object parent, Object node)

    Called after all of the children of a node have been created.

  • setParent(Object parent, Object child)

    Called after createNode for each child node, in order to allow any parentchild
    relationships to be established.



BuilderSupport takes care of all of the nitty-gritty of handling
pretended methods for us. Had our PoorMansTagBuilder worked only
for parameter-less tags, BuilderSupport would have detected which
type of call is being made and called the appropriate createNode for us.
The setParent method is only called if a parent node exists.


How this all hangs together is best illustrated by way of an example. So let’s start by
creating a really dumb builder that just logs these methods as they are encountered.
This will give us a feel for the sequence in which the methods are called.



class LogBuilder extends BuilderSupport {
def indent = 0
def createNode(name){
indent.times {print ” “}
println “createNode(${name})”
indent++
return name
}
def createNode(name, value){
indent.times {print ” “}
println “createNode(${name}, ${value})”
indent++
return name
}
def createNode(name, Map attributes){
indent.times {print ” “}
println “createNode(${name}, ${attributes})”
indent++
return name
}
def createNode(name, Map attributes, value){
indent.times {print ” “}
println “createNode(${name}, ${attributes}, ${value})”
indent++
return name
}
void setParent(parent, child){
indent.times {print ” “}
println “setParent(${parent}, ${child})”
}
void nodeCompleted(parent, node) {
indent–
indent.times {print ” “}
println “nodeCompleted(${parent}, ${node})”
}
}


To use this builder, all that we need to do is to construct one and start writing some
markup with it. Here we have some markup for building customer records, but
as this builder does not care what the method tags are, we could write whatever
markup we please.



def builder = new LogBuilder()
def customers = builder.customers {
customer{
id(1001)
name(firstName:”Fred”,surname:”Flintstone”)
address(“billing”, street:”1 Rock Road”,city:”Bedrock”)
address(“shipping”, street:”1 Rock Road”,city:”Bedrock”)
}
}


If we run this script, we will generate the following output. We can see from the
output exactly what the sequence of calling is, and what parameters are being
passed. We’ve used this simple example for illustrating how the BuilderSupport
class works, but it is actually a useful debugging tool in general for using with any
builder that’s not behaving as expected. By replacing any existing builder instance
in your code with a LogBuilder, it will output the construction sequence for you,
which may identify the problem.



createNode(customers)
createNode(customer)
setParent(customers, customer)
createNode(id, 1001)
setParent(customer, id)
nodeCompleted(customer, id)
createNode(name, [firstName:Fred, surname:Flintstone])
setParent(customer, name)
nodeCompleted(customer, name)
createNode(address, [street:1 Rock Road, city:Bedrock], billing)
setParent(customer, address)
nodeCompleted(customer, address)
createNode(address, [street:1 Rock Road, city:Bedrock], shipping)
setParent(customer, address)
nodeCompleted(customer, address)
nodeCompleted(customers, customer)
nodeCompleted(null, customers)


From this output, we can trace the sequence in which the hooks are called. Nodes
are created from the top down. The createNode hook for the parent is called
first. The createNode hook for a child is called next, and setParent is called for
each individual child after both the parent and the child have been created. The
nodeCompleted hook is called only after all of the children have been created and
their parent-child relations set.


The default implementation of BuilderSupport manages the current node cursor
by itself. Two additional hooks to consider are:



  • setCurrent(Object current)

  • Object getCurrent()


Certain builder implementations might want to manage the notion of a “current”
node object in order to maintain a cursor on the construction process. If so, both of
these hooks will need to be implemented.


Now that we understand the building mechanism, it is a trivial matter to change our
LogBuilder script to create some actual markup. Here, with a few modifications, we
can turn our script into PoorMansTagBuilder 2.0.



class PoorMansTagBuilder20 extends BuilderSupport {
def indent = 0
def createNode(name){
indent.times {print ” “}
println “<${name}>”
indent++
return name
}
def createNode(name, value){
indent.times {print ” “}
println “<${name}>” + value
indent++
return name
}
def createNode(name, Map attributes){
indent.times {print ” “}
print “<${name} ”
print attributes.collect {
“${it.key}=’${it.value}’”
}.join(‘ ‘)
println “>”
indent++
return name
}
def createNode(name, Map attributes, value){
indent.times {print ” “}
print “<${name} ”
print attributes.collect {
“${it.key}=’${it.value}’”
}.join(‘ ‘)
println “>” + value
indent++
return name
}
void setParent(parent, child){
// Don’t care since we are just streaming to output
}
void nodeCompleted(parent, node) {
indent–
indent.times {print ” “}
println “</${node}>”
}
}


Once again, this is a simple implementation of a tag builder. We are making no
interpretation of the method tags that are being passed in, so for each createNode
call all we do is output a <TAG> with parameters and attributes if necessary. The
setParent call is not relevant to us because we are just streaming output to standard
out. We will see in the next example where we need to implement this. Finally, the
nodeCompleted call just closes the tag </TAG>.


Now we can apply this builder to the same customers markup script
that we did before, as follows. The only change required is to instantiate a
PoorMansTageBuilder20 in place of the original builder class.



def builder = new PoorMansTagBuilder20()
def customers = builder.customers {
customer {
id(1001)
name(firstName:”Fred”,surname:”Flintstone”)
address(“billing”, street:”1 Rock Road”,city:”Bedrock”)
address(“shipping”, street:”1 Rock Road”,city:”Bedrock”)
}
}


Running this script will log some reasonably well-formed XML as follows:



<customers>
<customer>
<id>1001
</id>
<name firstName=’Fred’ surname=’Flintstone’ >
</name>
<address street=’1 Rock Road’ city=’Bedrock’ >billing
</address>
<address street=’1 Rock Road’ city=’Bedrock’ >shipping
</address>
</customer>
</customers>


As a markup builder, this falls well short of the features in the Groovy
MarkupBuilder, but it does show just how easy it is to put together a quick builder
to fit the need of the day. Now let’s consider what we’ve learned, and look at
building something a little more useful.


A database builder


Every application that includes a database needs some means of setting up seed, demo,
or test data. ihave worked on numerous enterprise applications during my career and
invariably the management of different data sets becomes as much of an effort over
time as the development of the application itself. In my own experience of working
with Independent Software Vendors (ISVs), whose applications need to be deployed
on multiple customer sites with multiple versions, the problem becomes acute.


ISV companies often have competing needs for data sets. The sales organization
needs a predictable data set for its demos to customers. The test department
needs data sets that allow them to test specific features of the application. Project
management requires specific seed data to be available, which is tailored to each
customer site prior to installation. With all of these competing requirements, the IT
department has a limited set of database instances available on which to install and
test all of these configurations.


There are various ways of managing data sets. The most common is to maintain
SQL scripts that take care of the database insertions. Building a simple database will
require multiple insertions into a multitude of tables. The SQL script needs to be
written in such a way as to maintain the integrity of foreign key references. It’s
not an easy thing to do, and requires intimate knowledge of the schema.


Suppose we are working with a Grails application. Take for example the one-tomany
relationship we looked at in Chapter 6:



class Customer {
String firstName
String lastName
static hasMany = [invoices:Invoice]
}
class Invoice {
static hasMany = [orders:SalesOrder]
}
class SalesOrder {
String sku
int amount
Double price
static belongsTo = Invoice
}


Grails has a migration plug-in that can be installed. The migration tool will execute
a SQL update script to migrate our database between versions. To use the migrate
tool to add some simple test data for the above classes, we need to know how GORM
maps from the Groovy POGO objects to relational tables.


In Chapter 6, we also saw how these classes in fact map to five tables in the relational
database. There are three main tables that represent the business objects (customer,
invoice, and sales_order) and there are two mapping tables used to manage the
foreign key mappings (customer_invoice and invoice_sales_order) that relate
customers to invoices and invoices to sales orders.


To set up a simple test case with one customer, one invoice and three sales orders
would require nine insertions across these tables. Apart from being error prone
and difficult to maintain, the script will be incomprehensible to anyone who is not
intimately acquainted with SQL. What starts out as a simple data input spec for a
test case becomes a development task for a domain SQL expert who understands the
GORM mapping model.


An alternative to this approach is to use the GORM APIs to build the test data. At
least if we do this then we don’t have to concern ourselves with the foreign key
relationships between tables. The script below will set up our simple data set
with one customer, one invoice, and three sales orders:



def fred = new Customer(firstName:”Fred”, lastName:”Flintstone”)
fred.save()
def invoice = new Invoice()
invoice.addToOrders(new SalesOrder(sku:”productid01″, amount:1,
price:1.00))
invoice.addToOrders(new SalesOrder(sku:”productid02″, amount:3,
price:1.50))
invoice.addToOrders(new SalesOrder(sku:”productid03″, amount:2,
price:5.00))
fred.addToInvoices(invoice)


This is somewhat better than the SQL script approach, but it does impose a
procedural construction onto the data, where the test data is typically defined
declaratively. While I’ve used GORM to illustrate my point here, the same issues
will crop up whatever persistence mechanism we use.


Ideally, we want to be able to describe our data in a declarative style. The syntax of the
data definition should as closely match the structure of the resulting data as possible.
This is an ideal situation in which to use a builder to take care of construction. With a
builder, it should be possible to create a declarative markup style script for building
data sets. The builder can take care of the complexities of construction.


Let’s first of all imagine how a builder for customers may look in use. We probably
want to handle multiple customers, so a top-level customers method is useful. We
could have multiple customer blocks nested below that. Nesting is a good way of
depicting ownership in a one-to-many relationship, so our Customers markup would
probably look something like the following:



builder.customers {
customer{
invoice {
salesOrder()
salesOrder()
salesOrder()
}
}
}


We need to be able to set the fields of each entity as it is created. We could have a
pretended method for each field as follows:



builder.customers {
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)
}
}
}
}


This will work. However, it is not immediately clear to a reader of this script that
lastName is an object attribute and invoice is a new subsidiary object. A better option
is to set object attributes as mapped parameter values. The following script is far
clearer in its intent, so this is the one we will try to implement:



builder.customers {
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)
}
}
}


As it happens, the BuilderSupport hook methods and their calling sequence
work perfectly in step with the GORM APIs that we need in order to construct
our customer records.



  • The createNode method will be called in the correct top down sequence,
    allowing us to create the appropriate Customer, Invoice, or SalesOrder
    as required.

  • The setParent hook is called after both parent and child objects have
    been constructed, allowing us to call Customer.addToInvoices or
    Invoice.addToOrders when we need to.

  • The nodeCompleted hook can be used to intercept when an object needs
    to be saved.


The following code snippet contains a rudimentary builder class based on
BuilderSupport that constructs customer, invoice, and sales order objects through
GORM. The same style of builder could work equally well with whatever other
persistence method we choose.



class CustomersBuilder extends BuilderSupport {
def createNode(name){
Object result = null
switch (name) {
case “customer”:
return new Customer(firstName:”", lastName:”")
case “invoice”:
return new Invoice()
case “salesOrder”:
return new SalesOrder(sku:”default”,amount:1,price:0.0)
}
}
def createNode(name, value){
Object result = createNode(name)
if (value instanceof Customer && result instanceof Invoice)
value.addToInvoices(result)
if(value instanceof Invoice && result instanceof SalesOrder)
value.addToOrders(result)
return result
}
def createNode(name, Map attributes){
Object result = null
switch (name) {
case “customer”:
return new Customer(attributes)
case “invoice”:
return new Invoice(attributes)
case “salesOrder”:
return new SalesOrder(attributes)
}
}
def createNode(name, Map attributes, value){
Object result = createNode(name,attributes)
if (value instanceof Customer && result instanceof Invoice)
value.addToInvoices(result)
if(value instanceof Invoice && result instanceof SalesOrder)
value.addToOrders(result)
return result
}
void setParent(parent, child){
if (child instanceof Invoice && parent instanceof Customer)
parent.addToInvoices(child)
if (child instanceof SalesOrder && parent instanceof Invoice)
parent.addToOrders(child)
}
void nodeCompleted(parent, node) {
if (node != null)
node.save()
}
}


Here we have implemented all four createNode methods. The method tag is passed
as the name parameter to createNode. So we construct a Customer, Invoice, or
SalesOrder object based on the tag that we are processing. We allow a parent object
to be set in the value parameter to the method. This allows us to construct a child
object outside the scope of a parent, and set its parent later.


The setParent method takes care of adding invoices to customers and sales orders
to invoices. Testing the instanceof both parent and child ensures that we don’t
attempt to add an invoice if it is declared outside of the customer.


All that remains for nodeCompleted to do is to save the node object that we have
created to the database. When we put all of this together, we can make use of
our CustomerBuilder to build a simple test database as follows:



def builder = new CustomersBuilder()
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)
}


By allowing a parent object to be passed as the value parameter, we have made
the markup script more fl exible. As you can see from the preceding code, invoice
and salesOrder tags can be declared directly as children of a parent object, or they
can be declared independently. This gives us a bit more fl exibility in what types of
mapping relationships we can support where ownership between parent and child
might be optional, or in more complex scenarios where many-to-many relationships
might need to be declared.

Groovy Articles

email

«»

Comments

comments