Mittwoch, 9. März 2011

Interne DSL mit Groovy Teil 2

Im ersten Teil habe ich Techniken vorgestellt, mit denen man interne DSLs in Groovy erstellen kann. Im zweiten Teil werde ich zeigen, wie die Fragebogen-DSL aus dem Clojure Beitrag in Groovy aussehen könnte.

Fragebogengenerator als API


Man könnte den Fragebogengenerator mit einer API zur Verfügung stellen.

Klassendiagramm des Fragebogengenerators

In Groovy implementiert könnte das folgendermaßen aussehen.

enum Fragetyp {
    EINFACHAUSWAHL("radio"),
    MEHRFACHAUSWAHL("checkbox"),
    TEXTEINGABE("text")
    
    String html
    
    Fragetyp(html) {
        this.html = html
    }
}

class Frage {
    String frage
    Fragetyp typ
    List<String> antworten = []
    
    void antwortHinzufügen(String antwort) {
        antworten << antwort
    }
    
    String antwortZuHtml(String type, String name, String value) {
            String html = "<input type=\"" + type + "\" name=\"" + name + "\""
            if(value != null)
                html += "value=\"" + value + "\">" + value
            else
                html += "/>" 
            html += "<br /‍>"
    }
    
    String zuHtml() {
        String html = "<h2>" + frage + "</h2>"
        if(typ == Fragetyp.TEXTEINGABE)
            html += antwortZuHtml(typ.html, frage, null)
        else
            antworten.each { antwort -> 
                html += antwortZuHtml(typ.html, frage, antwort)    
            }
        return html
    }
}

class Fragebogen {
    String title
    List<Frage> fragen = []
    
    void frageHinzufügen(Frage frage) {
        fragen << frage
    }
    
    String zuHtml() {
        String html = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n"
        html += "<html><head><title>" + title + "</title></head><body><h1>" + title + "</h1><form action=\"\">"
        fragen.each { frage->
            html += frage.zuHtml()
        }
        html += "<br /‍><input type=\"submit\" value=\"Senden\" /></form></body></html>"
    }
}

Möchte man die API verwenden um einen Fragebogen zu erzeugen, sieht das folgendermaßen aus:

fragebogen = new Fragebogen(title:"Kfz-Umfrage")

frage = new Frage(typ:Fragetyp.EINFACHAUSWAHL, frage:  "Welche Marke hat ihr Fahrzeug?")
frage.antwortHinzufügen("Opel")
frage.antwortHinzufügen("Renault")
frage.antwortHinzufügen("BMW")
fragebogen.frageHinzufügen(frage)

frage = new Frage(typ:Fragetyp.TEXTEINGABE, frage: "Wie lautet die Modelbezeichnung?")
fragebogen.frageHinzufügen(frage)

frage = new Frage(typ:Fragetyp.MEHRFACHAUSWAHL, frage:  "Welche Ausstattungsmerkmale hat ihr Fahrzeug?")
frage.antwortHinzufügen("Klimaanlage")
frage.antwortHinzufügen("Navigationssystem")
frage.antwortHinzufügen("ESP")
fragebogen.frageHinzufügen(frage)

println(fragebogen.zuHtml())

Eine General Purpose Language wie Groovy oder Java muss Problemstellungen aus vielen unterschiedlichen Domains abbilden können. So eine Sprache muss sehr flexibel sein. Dadurch ist viel technischer Overhead nötig um ein Problem aus der Domain "Fragebogen erstellen" zu beschreiben. Die Flexibilität von Groovy zeigt sich in diesem Beispiel daran, dass man z.B. mehrere Fragebögen auf einmal erstellen könnte. Anschließend könnte man Fragen erstellen und diese zu verschiedenen Fragebögen zuweisen.

kfzFragebogen = new Fragebogen(title:"Kfz-Umfrage")
handyFragebogen = new Fragebogen(title:"Handyfragebogen")

frage = new Frage(typ:Fragetyp.EINFACHAUSWAHL, frage:  "Welche Marke hat ihr Fahrzeug?")
frage.antwortHinzufügen("Opel")
frage.antwortHinzufügen("Renault")
frage.antwortHinzufügen("BMW")
kfzFragebogen.frageHinzufügen(frage)

frage = new Frage(typ:Fragetyp.MEHRFACHAUSWAHL, frage:  "Was für Betriebssystem läuft auf ihrem Handy?")
frage.antwortHinzufügen("Android")
frage.antwortHinzufügen("iOS")
frage.antwortHinzufügen("Windows Phone 7")
handyFragebogen.frageHinzufügen(frage)

println(kfzFragebogen.zuHtml())
println(handyFragebogen.zuHtml())

Fragebogengenerator als DSL


Das macht in der Domäne "Fragebogen erstellen" nicht viel Sinn. Diese Flexibilität ist hier nicht unbedingt nötig. Legt man z.B. fest, dass eine erstellte Frage automatisch zu dem zuletzt erstellten Fragebogen gehört, lässt sich viel technischer Overhead sparen. Die eingebüßte Flexibilität fällt nicht auf, da man es in den meisten Fällen ohnehin so gemacht hätte. Eine Beschreibung des Kfz-Fragebogen in einer internen DSL für diese Domain könnte dann folgendermaßen aussehen:

fragebogen "Kfz-Umfrage"
  frage einfachauswahl, "Welche Marke hat ihr Fahrzeug?"
    antwort "Opel"
    antwort "Renault"
    antwort "BMW"
  frage texteingabe, "Wie lautet die Modelbezeichnung?"
  frage mehrfachauswahl, "Welche Ausstattungsmerkmale hat ihr Fahrzeug?"
    antwort "Klimaanlage"
    antwort "Navigationssystem"
    antwort "ESP"

In dieser Repräsentation ist fast kein technischer Overhead mehr vorhanden. Somit kann auch ein Nicht-Programmierer erkennen um welche Domäne es hier geht. Nach kurzer Einarbeitungszeit kann der Nicht-Programmierer auch eigene Fragebögen erstellen oder vorhandene bearbeiten.

Damit das so funktioniert sind ein paar Wrapper-Funktionen für die weiter oben beschriebene API nötig.

aktuellerFragebogen = null
aktuelleFrage = null

void fragebogen(String title) {
    aktuellerFragebogen = new Fragebogen(title: title)
}

void frage(Fragetyp typ, String frage) {
    aktuelleFrage = new Frage(frage:frage, typ:typ)
    aktuellerFragebogen.frageHinzufügen(aktuelleFrage)
}

void antwort(String antwort) {
    aktuelleFrage.antwortHinzufügen(antwort)
}

einfachauswahl = Fragetyp.EINFACHAUSWAHL
mehrfachauswahl = Fragetyp.MEHRFACHAUSWAHL
texteingabe = Fragetyp.TEXTEINGABE
binding['fragebogen'] = this.&fragebogen
binding['frage'] = this.&frage
binding['antwort'] = this.&antwort

Fragebogengenerator als Interpreter


Mit dem folgenden Code wird es schließlich möglich die Fragebogen-DSL als Interpreter zu starten. Dieser nimmt einen Pfad zu einem Fragebogen entgegen, führt diesen aus und gibt schließlich den Fragebogen als HTML aus.

void ausführungAbbrechen () {
 println("Bitte geben Sie den Pfad zum Fragebogen als Parameter an.")
 System.exit(1)
}

if(this.args.length != 1)
 ausführungAbbrechen()
File eingabe = new File(args[0])
if(!eingabe.exists())
 ausführungAbbrechen()
new GroovyShell(binding).evaluate(eingabe)
    
println aktuellerFragebogen.zuHtml()

Das ganze kann man jetzt direkt ausführen,

groovy FragebogenDsl.groovy

oder kompilieren und eine runnable-Jar erstellen,

mkdir interpreter
cd interpreter
groovyc -d . X:\Pfad\zu\FragebogenDsl.groovy
jar -xf X:\Pfad\zu\Groovy\embeddable\groovy-all-*.jar
jar cvef FragebogenDsl Fragebogengenerator.jar *

welche man im Anschluss wie üblich ausführen kann.

java -jar Fragebogengenerator.jar

Links

Keine Kommentare:

Kommentar veröffentlichen