dsl mit groovy

erstellt am 02.06.2008 | 1 kommentare

themen: groovy, mda/mdd, dsl

groovy-dsl-tutorial

die script-sprache groovy (http://groovy.codehaus.org) eignet sich gut zur erstellung einer textbasierten dsl (domain specific language).

in diesem artikel soll anhand eines kleinen beispiels eine mgöliche vorgangsweise demonstriert werden.

ziel ist eine einfache dsl zur beschreibung von datenbank-tabellen, um daraus ein sql-statement zur erzeugung der tabelle zu generieren.
(anmerkung: das ist natürlich nur eine demonstration der möglichkeiten, die sinnhaftigkeit des beispiels soll hier nicht zur debatte stehen.)

hier die angestrebte dsl:

1 table("test") {
2 column "id", datatype: "int", notnull: true
3 column "name", datatype: "text"
erklärung:
in zeile 1 wird die tabelle "test" definiert, die 2 columns hat:
in zeile 2 wird die column "id" definiert, deren datentyp int ist und die zwingend ausgefüllt sein muss (not null)
in zeile 3 wird die column "name" mit dem datentyp text definiert

das ergebnis soll ein create-statement mit folgendem aussehen sein:

create table test (
   id   int   not null,
   name   text
);
hier nun die schritte im einzelnen:

vorbemerkung: groovy hat ein "builder"-konzept, das sich hervorragend für diese art der dsl eignet. mit dem groovy-builder können alle hierarchischen sachverhalte verwaltet werden.
es ist in diesem beispiel nicht zwingend nötig, ich empfehle aber trotzdem die verwendung der FactoryBuilderSupport-klasse, die die abarbeitung einzelner tags (wie hier table und column) in eigene factories auslagert. dadurch wird der builder übersichtlicher, leichter erweiterbar und wartbarer.

model: als erstes sollten wir ein daten-model definieren, in dem wir die informationen über tabellen und columns speichern.

dazu erzeugen wir 2 klassen:
TableModel.groovy:

class TableModel {
   /**
    * Name der Tabelle
    */
   String name
   /**
    * Liste der Columns
    */
   ArrayList<ColumnModel> columns = new ArrayList<ColumnModel>()
}
und ColumnModel.groovy
class ColumnModel {
    /**
    * Tabelle, zu der diese Column gehört
    */
    TableModel table
    /**
    * name der column
    */
    String name
    /**
    * datentyp
    */
    String datatype
   /**
    * not null
    */
   boolean notnull
}
die beiden klassen definieren ihre klassen-variablen, die tabelle besitzt noch eine liste der columns, die column eine referenz auf die tabelle (diese referenz ist in unserem beispiel nicht nötig, bei komplexeren anwendungen ist es durchaus sinnvoll, zugriff auf sein master-objekt zu haben).
bei der definition der attribute ohne sichtbarkeits-hinweise (public, protected, private) macht groovy sogenannte properties daraus, die automatisch über zugriffsmethoden getXxx und setXxx verfügen und auch als object.xxx angesprochen werden können.
obwohl es in groovy nicht nötig ist, datentypen anzugeben (wir könnten auch def name schreiben), ist es aus meiner sicht sinnvoll, den datentyp anzugeben, sofern er feststeht und unveränderlich sein soll.

als nächstes erstellen wir die builder-klasse
TableBuilder.groovy:

class TableBuilder extends FactoryBuilderSupport {

   public TableBuilder() {
registerTags()
}

protected void registerTags() {
registerFactory("table", new TableFactory())
registerFactory("column", new ColumnFactory())
}
}

hier wird für jeden verwendeten tag eine factory registriert.
die beiden factory-klassen werden als nächstes implementiert:
TableFactory.groovy:
class TableFactory extends AbstractFactory {
   /**
    * Erzeugen Class
    */
Object newInstance( FactoryBuilderSupport builder, Object name, Object value, Map attributes )
throws InstantiationException, IllegalAccessException {
      attributes += [name : value]
   return new TableModel(attributes)
}
}
ColumnFactory.groovy:
class ColumnFactory extends AbstractFactory {
   /**
    * Erzeugen Column
    */
Object newInstance( FactoryBuilderSupport builder, Object name, Object value, Map attributes )
throws InstantiationException, IllegalAccessException {
attributes += [name : value]
return new ColumnModel(attributes)
}

   /**
    * add Column to Table
    */
   public void setParent( FactoryBuilderSupport builder, Object parent, Object child ) {
      child.table = parent
      parent.columns += child
   }
}

in der methode newInstance wird jeweils eine neue instanz unserer model-klasse erzeugt.
hinweis zur zeile attributes += [name : value]:
mit groovy kann man ein objekt erzeugen, in dem man im konstruktor eine map übergibt. existieren kein konstruktor mit einzigem parameter vom typ map und ein standard-konstruktor ohne parameter, so erzeugt groovy ein objekt mit diesem standard-konstruktor und setzt dann die werte der properties anhand der übergebenen map.
da die factory in unserem beispiel jedoch den namen der tabelle bzw. column nicht in der map sondern im value übergeben bekommt, müssten wir eigene konstruktoren mit map und name erzeugen. um uns das ersparen, erweitern wir einfach die übergebene map um den namen mit dem wert, der in value steht. damit kann der groovy-automatismus verwendet werden.

die methode setParent in der ColumnFactory ermöglicht es uns, die beziehung zwischen tabelle und column zu setzen. in der column (child) wird die refernz auf die tabelle (parent) gesetzt, in der tabelle wird eine column in die liste der columns hinzugefügt.

anwenden: damit sind wir schon so weit, dass wir mit dem script

def builder = new TableBuilder();

def table = builder.table("test") {
   column "id", datatype: "int", notnull: true
   column "name", datatype: "text"
}

ein model der tabelle samt columns erzeugen können.

anmerkung: dank groovy können klammern in funktionsaufrufen wegfallen. deshalb ist statt

   column ("id", datatype: "int", notnull: true)
auch folgende schreibweise möglich:
   column "id", datatype: "int", notnull: true
   

jetzt fehlt nur mehr die ausgabe des create-statements, die dank der kompaktheit von groovy nur wenige zeilen umfasst:
die klasse TableModel wird um die methode create() erweitert:

   /**
    * erzeugt das create-statement für die tabelle
    */
   String create() {
      return "create table ${name} (\n" +
      (columns.collect {
         "\t" + it.create();
      }).join(",\n") + "\n);"
   }
und die klasse ColumnModel ebenfalls:
   /**
    * erzeugt das create-statement für die column
    */
   String create() {
      return name + "\t" + datatype + (notnull?"\tnot null":"")
   }
   

nun erweitern wir das scipt noch um die ausgabe:

def builder = new TableBuilder();

def table = builder.table("test") {
   column "id", datatype: "int", notnull: true
   column "name", datatype: "text"
}

println table.create()

das ergebnis kann natürlich auch gleich in eine sql-datei gespeichert oder über eine jdbc-connection direkt ausgeführt werden.

fazit: mit groovy kann mit wenig code eine dsl definiert werden.
mit dem builder-konzept und der kompakten syntax zeigt groovy hier seine volle stärke.

die source-files zu diesem tutorial können sie hier downloaden.

danke für ihre aufmerksamkeit,
ich feue mich über kommentare und anregungen.


kommentar abgeben

kommentare

  • hallo. wirklich ein gelungener artikel für den einstieg in die welt der dsls.
    gruß breskeby

    erstellt von René Gröschke, 05.03.2009 23:29 (vor 1jahr)

rss feed für kommentare dieser seite