DSL’s met Groovy builders

26 January 2009 17:20 Peter Brouwer Groovy

De laatste tijd heb ik veel met Groovy mogen doen, wat naast Java een van de vele talen is die op de JVM draait. Groovy heeft vele features, zoals closures en meta-programmering van andere dynamische talen als Python en Ruby overgenomen. Tegelijkertijd integreren groovy en java naadloos met elkaar, waardoor java programmeurs zich gelijk thuis voelen met groovy, ze kunnen immers al hun gebruikelijke klasses en api’s ook vanuit groovy gebruiken en aanroepen. Het is begrijpelijk dat deze taal met moderne features gecombineerd met de mogelijkheid de vele libraries van het Java platform te kunnen gebruiken de laatste tijd een hot topic is. Een ander hot topic van 2008 waren DSL’s (Domain Specific Languages). DSL’s zijn kleine subtaaltjes die speciaal ontworpen zijn om een specifiek probleem op te lossen. Zo kan SQL bijvoorbeeld als een DSL beschouwd worden om het probleem op te lossen hoe je interacteert met een relationele database. Ruby on Rails word door sommigen beschouwd als een DSL om het probleem “hoe maak ik een webapplicatie” op te lossen, al heb je het in het algemeen over meer specifieke problemen als je over DSL’s spreekt. In java wordt vaak XML gebruikt om domein specifieke problemen op te lossen, denk aan hibernate mappings, maven build files, etc. In deze post wil ik laten zien hoe je met Groovy Builders DSL’s kunt maken die gerelateerd zijn aan het probleem hoe je hierarchische data-structuren kan opbouwen.

Groovy Builders:

Groovy builders zijn klasses die overerven van de groovy.util.BuilderSupport klasse. Groovy stelt standaard een aantal builders ter beschikking, zoals de MarkupBuilder, voor het genereren van XML en de SwingBuilder, voor het genereren van Swing gebruiker interfaces. Stel je wilt graag het volgende XML document genereren vanuit een groovy programma:

<company id="Finalist">
  <blogarticle> 
    <title>Solving sodoku</title>
  </blogarticle>
  <blogarticle>
    <title>Groovy Builders</title>
  </blogarticle>
</company>

Dan is het mogelijk om de ingebouwde MarkupBuilder te gebruiken om deze XML te genereren. De groovy code komt er dan zo uit te zien:

import groovy.xml.MarkupBuilder
def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.company(id:'Finalist') {
  blogArticle() {
    title('Solving sodoku')
  }
  blogArticle() {
    title('Groovy Builders')
  }
}
println writer.toString()

Wat opvalt aan bovenstaande code is dat de structuur van het XML document gelijk duidelijk wordt aan de hand van de code die je op je scherm ziet. Dit in tegenstelling tot wanneer je bijvoorbeeld aan de slag gaat met de standaard Java DOM api, waar je metodes als appendChild() moet aanroepen om deze structuur te bereiken, je krijgt dan zoiets als:

Element company = doc.createElement("company");Element blogArticle = doc.createElement("blogArticle");
doc.appendChild(company);
company.appendChild(blogArticle);
//etc..

In bovenstaand Java voorbeeld is de structuur van het XML document niet in een opslag duidelijk, je moet echt de code regel voor regel lezen om te snappen hoe de XML structuur in elkaar zit.

Ok, dus met de ingebouwde MarkUpBuilder kan je met mooie code XML genereren, en met de ingebouwde SwingBuilder kan je mooie Swing code schrijven waaruit gelijk de hierarchische struktuur van je Frames/panels/labels/textvelden etc duidelijk wordt. Maar wat als je iets heel anders wilt, misschien ben je wel een schaakprogramma aan het schrijven en wil je alle bekende schaakopeningen invoeren in je programma. In dat geval is het ook mogelijk om je eigen Builder te schrijven. Je schaakopeningen structuur is een hierarchische structuur die je misschien graag zo in je code wilt opschrijven:

def chess = new ChessBuilder()
chess.e4(comment : "Meest gebruikelijke opening voor wit"){
  e5(comment : "Zwarts meest gebruikelijke antwoord"){
    d4()
  }
  d5()
}

Het is mogelijk om deze syntax te bereiken door een klasse te schrijven die overerft van groovy.util.BuilderSupport. Laten we deze klasse ChessBuilder noemen. Deze klasse moet tenminste de 5 abstracte metodes van Buildersupport implementeren:

import groovy.util.BuilderSupport;
 
class ChessBuilder extends BuilderSupport{
 
public ChessBuilder(){
 
  void setParent(Object parent, Object child){
    println("Parent of ${child} is ${parent}")
  }
 
  def Object createNode(Object name){
    println("Creating: ${name}")
    return name
  }
 
  def Object createNode(Object name, Object value){
  }
 
  def Object createNode(Object name, Map attributes){
    println("Creating: ${name} with params: ${attributes}")
    return name
  }
 
  def Object createNode(Object name, Map attributes, Object value){
  }
}

Onze output wordt nu:
Creating: e4 with params: ["comment":"Meest gebruikelijke opening voor wit"] Creating: e5 with params: ["comment":"Zwarts meest gebruikelijke antwoord"] Parent of e5 is e4 Creating: d4 Parent of d4 is e5 Creating: d5 Parent of d5 is e4

We zien dat de metode createNode(Object name) word aangeroepen elke keer dat we een metode aanroep doen op ons builder object in de vorm van chess.xxx(). e4 is dan het object dat meegegeven word als parameter aan de metode. De metode def Object createNode(Object name, Map attributes) wordt aangeroepen elke keer als we een metode aanroep doen in de vorm van chess.foo(comment : “iets”). Na elke createNode() wordt ook de setParent() metode aangeroepen. (Behalve na het aanmaken van de root node, deze heeft immers geen parent.) Op deze manier kunnen we een hierarchische structuur van onze schaakopeningen opbouwen en hebben we een mooie kleine DSL gemaakt specifiek voor het invoeren van schaakopeningen!

1 reactie »

  1. De code in het laatste voorbeeld is een beetje stuk, er mist een harde return na de puntkomma in het import statement en een accolade sluiten na de opening van de constructor.

    Je kunt trouwens de returntypes weglaten, die heeft groovy niet nodig… en zeggen in het geval van Object verder ook niks nuttigs.

    Peter Maas January 26, 2009 17:59

Reageer

RSS feed for comments on this post · TrackBack URI