DZone

Friday, May 6, 2011

Groovy Builders and Modularity

Groovy Builders are a very nice way for hiding 3rd-party APIs complexity, and help you cleaning up a lot your code, but when the thing to be built is large, the method where the builder is used gets really huge. In this post I will explain you the way to split the builder block and make it modular.

Let's start analyzing how Builders work commonly. The builder uses to have an entry-point method which takes a closure as parameter, within the closure you can invoke any builder method to start configuring the target artifact.

Let me show it with an example, I'll use Grails' HibernateCriteriaBuilder and a well know domain for us (developers) which is Issue Tracking:

We have a Map populated from a form holding filtering values:

def filter = fromFilterForm()

def criteria = Issue.createCriteria()

def issues = criteria.list {

  if(filter.author) {
    eq('author', filter.author)
  }

  if(filter.title) {
    like('title', "%${filter.title}%")
  }

  if(filter.description) {
    like('description', "%${filter.description}%")
  }

  if(filter.priority) {
    ge('priority', filter.priority)
  }

  if(filter.created) {
    between('created', filter.created.from, filter.created.to)
  }

  if(filter.statuses) {
    inList('status', filter.statuses)
  }

}

It is not the case for a huge building closure, but you can figure out where I'm pointing to.

So now, I will change it to achieve the same result but in a modular way. For it, I will move all the builder related lines to separate closures held by a map which shares the same keys than the filter map. And I will use a very helpful method in Closure called delegate(Object) so I can propagate the same behavior in HibernateCriteriaBuilder#list(Closure) to all the tiny closures.
If a key is not present in filter map, because it's not desired to filter by it, the closure under the same key in the closures map won't be invoked so we can avoid using that much if statements.

void initClosures() {
  this.closures = [
    author: { eq('author', it) },
    title: { like('title', "%${it}%") },
    description: { like('description', "%${it}%") },
    priority: { ge('priority', it) },
    created: { between('created', it.from, it.to) },
    statuses: { inList('status', it) }
  ]
}

void buildCriteria(def criteria, def filter) {
  filter.each{
    if(it.value){
      def c = this.closures[it.key]
      c.delegate = criteria
      c(it.value)
    }
  }
}

void filterIssues() {
  def filter = fromFilterForm()

  def criteria = Issue.createCriteria()

  criteria.list {
    buildCriteria criteria, filter
  }
}

This is just an example for showing you up how to write a more modular builder code, but you can apply this approach everywhere you need it.

Questions, comments and corrections are welcome.
Good coding to everyone.

Gaston.