Dienstag, 19. April 2011

Erstellung von Constraints mit der DSL

Ein großer Vorteil interner DSLs ist, dass man auf der Syntax und der Semantik einer Hostsprache aufbauen kann und somit bei der Entwicklung einer DSL nicht bei Null angefangen werden muss. Ein großer Nachteil interner DSLs ist, dass man auf der Syntax und der Semantik der Hostsprache aufbauen muss und somit nicht so leicht neue Sprachkonstrukte implementiert werden können.
Jedes Sprachkonstrukt, das man z.B. in einer internen Groovy-DSL verwenden möchte, muss vom Groovy-Parser als gültiger Groovy-Code erkannt werden. Möchte man auf AST Transformation verzichten, müssen die Sprachkonstrukte auch semantisch Groovy entsprechen. Um überhaupt eine interne DSL erstellen zu können, müssen deshalb oft vorhandene Sprachkonstrukte der Hostsprache Groovy genutzt werden und auf so untypische Weiße verwendet werden, dass sie zwar gültiger Groovy-Code bleiben, aber vom Aussehen her auch als Sprachelemente einer "neuen" Sprache interpretiert werden könnten. Das macht die Entwicklung der DSL schwierig, da oft auf etablierte Konventionen verzichtet werden muss. Als Beispiel für dieses Problem wird in diesem Artikel die Erarbeitung der Syntax der Akzeptanztest-DSL für die Erstellung von Constraints vorgestellt.

Erstellung von Constraints mit der DSL


Mit der DSL soll es unter anderem möglich sein, Einschränkungen (bzw. Constraints) formulieren zu können. Idealerweise sollte der Benutzer die Constraints so formulieren können, wie er es bereits, von verschiedenen Programmiersprachen, gewohnt ist. Das könnte folgendermaßen aussehen:
Testuser = User.where {
  Nickname == "Testuser"
}

Poweruser = User.where {
  postcount > 100
}

Die hier formulierten Constraints sollen keinen Wahrheitswert zurück liefern. Sie sollen verwendet werden, um SQL-Queries zu generieren. Auf den ersten Blick scheint das ohne weiteres möglich zu sein, da Groovy Operator Overloading unterstützt. Auf den zweiten Blick stellt sich aber heraus, dass man Vergleichsoperatoren nicht individuell überladen kann. Am besten deutlich wird das an einem Codebeispiel:
class Foo implements Comparable {
  def plus(other) {
    //Hier werden die Aktionen durchgeführt, die nötig 
    //sind um other zu dieser Instanz zu addieren.
  }

  def minus(other) {
    //Für die Subtraktion ist das ebenfalls möglich.
  }

  def equals(other) {
    //Auch den Vergleich auf Gleichheit kann man überladen.
  }

  int compareTo(other) {
    //Für die Operationen >,<,>= und <= gibt es allerdings nur
    //eine Methode.
  }
}

Diese Methode compareTo muss einen Wert zurückgeben, der größer, gleich oder kleiner null ist. Je nachdem ob other größer, gleich oder kleiner dieser Instanz der Klasse Foo ist. Groovy wendet auf diese Zahl dann den eigentlichen Vergleichsoperator an, um einen Wahrheitswert zu erzeugen. Im eigenen Code kann man also nicht ohne weiteres feststellen, welcher Vergleich durchgeführt wird. Dies ist aber nötig, um später einen SQL-Constraint erzeugen zu können. Die Möglichkeit zum individuellen überladen der einzelnen Vergleichsoperatoren soll, nach diesem Ticket, erst mit Version 2.0 möglich werden. In der Zwischenzeit wäre es gut eine andere Möglichkeit zu finden um dieses Problem doch so zu lösen.

Manipulation des abstrakten Syntaxbaumes


Groovy bietet die Möglichkeit, während der Kompilierung von Groovy- zu Bytecode, eigenen Code ausführen zu lassen, welcher in den Prozess eingreift. So kann man Modifikationen am abstrakten Syntaxbaum (AST) vornehmen, bevor aus diesem Bytecode generiert wird. In Groovy gibt es dafür die lokale- und die globale AST-Transformation. Diese AST-Transformationen müsste im Benutzer-Code durchgeführt werden, da die Closure, welche die Constraints enthält, bereits zu Bytecode kompiliert wurde, wenn sie der where Methode übergeben wird. Bei der lokalen AST-Transformation müssen die Stellen im Quelltext durch Annotationen markiert werden, bei denen die Transformationen durchgeführt werden sollen. Das würde in der DSL technischen Overhead bedeuten, welchen man den Benutzer nur schwer erklären könnte. Für die DSL käme also nur die globale AST-Transformation in Frage.
Die Idee ist jetzt, Vergleichsoperation, wie postcount > 100, durch Methodenaufrufe, wie greater(postcount, 100) zu ersetzen. Dabei müssen die Vergleichsoperationen, auf welche diese Transformation angewendet werden soll, sehr genau ausgesiebt werden, damit Nicht-DSL Groovy-Code weiterhin funktioniert. Denkbar wäre es, die Transformation nur auf Closures auszuführen, welche an where Methoden übergeben werden, welche genau eine Closure als Parameter übernehmen und in Klassen vorkommen, welche von AcceptanceUnit (im Moment die Superklasse für die DSL) erben. Das hätte allerdings den Nachteil, dass die Transformation auch auf Closures für where Methoden durchgeführt wird, bei denen das nicht erwünscht ist (z.B. bei where Methoden von Klassen von Dritt-Anbietern). Würde man das in Kauf nehmen bleibt das Problem, dass man den gesamten AST der entsprechenden Klassen durchforsten müsste um alle where-Methoden Aufrufe abfangen zu können. Das wäre mit großem Aufwand verbunden. Das Debugging ist ebenfalls nicht ohne weiteres möglich. Dabei spielt es keine Rolle, ob man das über einen Debugger oder über "println" macht. Das Problem ist, dass die AST-Transformation nicht als Teil des eigenen Programms läuft, sondern als Teil des Compilers.

Auswertung des Stacks


Eine andere Herangehensweise ist, die compareTo Methode in einer eigenen Klasse zu implementieren und sich in einem Debugger den Stack beim Aufruf dieser Methode anzusehen.



Dabei kann man feststellen, dass die eigene compareTo Methode, über Zwischenschritte, von der Methode compareGreaterThan der Klasse ScriptBytecodeAdapter aufgerufen wird. Für andere Vergleichsoperatoren wird die compareTo von anderen Methoden aufgerufen. Man kann sich in der compareTo Methode den aktuellen Stack besorgen und anhand der Methoden- und Klassennamen erkennen, welche Vergleichsoperation durchgeführt werden soll.

@Override
int compareTo(other) {
  def typ = null
  def stack = Thread.currentThread().getStackTrace()
  for(int index = 0; index < stack.length; index++) {
    if(stack[index].getClassName() !=
    "org.codehaus.groovy.runtime.ScriptBytecodeAdapter")
      continue
  switch(stack[index].getMethodName()) {
    case "compareLessThanEqual":
      typ = Constraint.Typ.LESS_OR_EQUAL; index = stack.length; break
    case "compareLessThan":
      typ = Typ.LESS; index = stack.length; break
    ...
  }
  ...
}

Das funktioniert für die Operatoren <, <=, >, >=. Dadurch kommt man aber zu einem weiteren Problem. Damit die Funktion compareTo für den Vergleich verwendet wird, muss die zugehörige Klasse das Interface implementieren. Sobald eine Klasse dieses Interface implementiert, gelten in Groovy aber andere Regeln für den Vergleich auf Gleichheit. In diesen Fällen überprüft Groovy, ob die zu vergleichenden Objekte Instanzen der gleichen Klasse sind. Ist das nicht der Fall, geht groovy davon aus, dass die Objekte nicht gleich sein können und ruft die benutzerdefinierte Funktion gar nicht erst auf. Somit kann man auf diese Weiße im Usercode keinen "Gleichheits"-Constraint erstellen.

Manipulation des Verhaltens von ScriptBytecodeAdapter zur Laufzeit mithilfe von MetaClass


Mithilfe von MetaClass kann man zur Laufzeit einer Klasse Methoden und Attribute hinzufügen. Das verändern vorhandener Methoden ist ebenfalls möglich. Die zuständige Instanz von MetaClass für eine bestimmte Klasse erhält man über das Attribut metaClass. Das funktioniert nicht nur für Groovy-Klassen, sondern auch für Javaklassen, wie das folgende Beispiel zeigt.

String.metaClass.hashCode = {->
    return 42
}
            
def test = "teststring"
println test.hashCode() //Ausgabe: 42

def test2 = "another teststring"
println test2.hashCode() //Ausgabe: 42

Die Idee ist nun, mithilfe dieses Mechanismus die Vergleichsmethoden, wie compareGreaterThan der Klasse ScriptBytecodeAdapter, zu überschreiben und ein eigenes Verhalten zu implementieren. Diese Methoden sind statisch, aber im Normalfall kann man auch diese überschreiben:

String.metaClass.static.test= {->
    println("test called")
}

String.test() //Ausagbe: test called


Für die Klasse ScriptBytecodeAdapter funktioniert dieser Mechanismus leider nicht. Auch für DefaultTypeTransformation ist das nicht möglich. Die selbst erstellte Methode wird in dem Fall einfach ignoriert und es wird die bereits vorhandene Methode verwendet. Das gleiche gilt z.B. auch, wenn man versucht die toString Methode der Klasse String zu überschreiben:

String.metaClass.toString = {->
    return "A"
}
            
def test = "B"
println test.toString() //Ausgabe: B


Veränderungen am Groovy-Quellcode


Groovy ist unter der Apache 2 Lizenz veröffentlicht und somit OpenSource Software. Man könnte also am Groovy Quellcode Veränderungen vornehmen, um das gewünschte Verhalten zu erreichen. Die Stellen im Quellcode welche verändert werden müssten, sind bereits identifiziert (in den Vergleichsmethoden wie compareGreaterThan der Klasse ScriptBytecodeAdapter). Die Veränderungen zu programmieren ist einfach und schnell möglich und da Groovy Maven als Abhängigkeitenverwaltungs- und Buildtool verwendet, ist die modifizierte Groovy-Version auch schnell erstellt. Allerdings wäre die DSL dann auf diese modifizierte Version angewiesen und man müsste sich selbst um die Wartung dieser Groovy-Version kümmern.

Auswertung des abstrakten Syntaxbaumes zur Laufzeit


Nachdem die alternative Syntax für die Formulierung von Constraints, welche im nächsten Abschnitt beschrieben wird, bereits implementiert wurde, habe ich von einer weiteren Möglichkeit erfahren, mit der man eventuell die Formulierung von Constraints doch wie geplant implementieren könnte.
Die Klasse MetaClass hat die Funktion getClassNode(). Mit dieser Funktion erhält man zur Laufzeit den abstrakten Syntaxbaum der entsprechenden Klasse. Das könnte man für die Closure nutzen, welche der where Methode als Parameter übergeben wurde. Dadurch besteht das Problem nicht, die Stellen im Code ausfiltern zu müssen, wie es im Abschnitt "Manipulation des abstrakten Syntaxbaumes" beschrieben ist. Da das Ganze zur Laufzeit passiert, muss der AST nicht manipuliert werden, sondern kann einfach ausgewertet werden.
Damit das funktioniert muss dafür gesorgt werden, dass der Quelltext der entsprechenden Closure zur Laufzeit zur Verfügung steht, da der AST ja aus dem Quelltext erzeugt wird. Die Akzeptanztests werden dem Programm später als Groovy-Skripte übergeben. Somit ist diese Bedingung leicht zu erfüllen.

Alternative Syntax für die Formulierung von Constraints


Da es mit den getesteten Ansätzen nicht gelungen ist, die Syntax für die Formulierung von Constraints wie geplant zu implementieren, wurde eine alternative Syntax implementiert. Diese Syntax lehnt sich an der Abfragesyntax von GORM an. Ein Constraint wird dabei durch einen Methodenaufruf beschrieben. Der Methode werden die Operatoren als Parameter übergeben. Das Beispiel von oben sieht mit der aktuellen Implementierung wie folgt aus:

Testuser = User.where {
  equal(Nickname, "Testuser")
}

Poweruser = User.where {
  greater(postcount,100)
}

Montag, 4. April 2011

Konzept der Akzeptanztest-DSL

Im letzten Beitrag wurde gezeigt, wie eine DSL für Akzeptanztests anhand der Anforderungen der Domäne "Forum" entwickelt wurde. In diesem Beitrag werden die Konzepte hinter diesem Ansatz einer Akzeptanztest-DSL erläutert.

Konzept der Akzeptanztest-DSL


Ein Ziel der DSL ist es, Akzeptanztests möglichst prägnant und mit möglichst wenig technischem Overhead formulieren zu können. Jeder, der mit den Akzeptanztests in Berührung kommt, soll schnell erkennen können, welche Bedingungen für erfolgreiche Durchführung der Tests nötig sind. Aus diesem Grund werden die Tests in der Form von Vor- und Nachbedingungen formuliert. Diese Art der Formulierung ist durch Use-Cases vielen bereits bekannt. Außerdem ist es dadurch möglich, die Vor- und Nachbedingungen der Use-Cases in den Akzeptanztests wiederzuverwenden.

Bei der Formulierung von Vor- und Nachbedingungen in natürlicher Sprache, werden Fachbegriffe der entsprechenden Domäne verwendet. Durch diese können Sachverhalte mit wenigen Worten präzise beschrieben werden. In der Akzeptanztest-DSL können aus demselben Grund ebenfalls Fachbegriffe verwendet werden. Damit das möglich ist, müssen alle verwendeten Fachbegriffe zunächst definiert werden. Fachbegriffe werden im Folgendem als Objekte einer Domäne betrachtet und als Domänenobjekte bezeichnet.

Domänenobjekte basieren auf anderen Domänenobjekten. „Basieren“ ist hier gleichbedeutend mit „ist ein“ (z.B. Ein Moderator ist ein Benutzer). Die Domänenobjekte der untersten Ebene sind 1 zu 1 Abbildungen von Datenbanktabellen. Jedes Domänenobjekt entspricht dabei genau einer Datenbanktabelle wobei jede Spalte der Tabelle genau einer Eigenschaft des Domänenobjektes der untersten Ebene entspricht. Dadurch wird es möglich einen SQL-Query generieren zu lassen.

Domänenobjekte bestehen aus Eigenschaften und Einschränkungen. Bei der Definition eines neuen Domänenobjektes wird das Basisobjekt als Grundlage geklont und um neue Eigenschaften und Einschränkungen erweitert. Das neue Domänenobjekt ist somit eine Spezialisierung des Basisdomänenobjektes. Ein Domänenobjekt ist nach seiner Definition nicht mehr veränderbar.

Die Eigenschaften eines Domänenobjektes sind Unterscheidungsmerkmale (z.B. Benutzer hat einen Nicknamen und einen Gruppennamen). Diese Unterscheidungsmerkmale bieten spezialisierten Domänenobjekten die Möglichkeit zur Spezialisierung. Vorgenommen werden diese Spezialisierungen durch Einschränkungen (z.B. Moderator ist ein Benutzer mit Gruppennamen = „Global Moderators“ oder Testbenutzer ist ein Benutzer mit Nicknamen = „Mustermann“).

Die Eigenschaften können neue Namen für Eigenschaften eines weiter unten in der Hierarchie liegenden Domänenobjektes sein (z.B. Benutzer basiert auf der Tabelle users. Benutzer hat die Eigenschaft Nickname. Nickname ist die Eigenschaft nick von Tabelle users). Eigenschaften können aber auch weitere Domänenobjekte mit eigenen Einschränkungen sein (z.B. Benutzer basiert auf der Tabelle users. Benutzer hat die Eigenschaft Gruppen. Gruppen sind alle Einträge aus der Tabelle groups bei denen die Eigenschaft id gleich der Eigenschaft group_id der Tabelle users ist.). Eigenschaften können aber auch Funktionen sein (z.B. Die Eigenschaft istEingeloggt ist wahr, wenn die Eigenschaft online > 0 ist, ansonsten ist sie falsch.).

Verwendung von objektorientierter Programmierung


Vergleicht man die hier verwendete Definition von Domänenobjekte mit der objektorientierten Programmierung, so kann man sich Domänenobjekte als Klassen vorstellen. Die Attribute dieser Klassen dienen zur Formulierung von Einschränkungen. An dieser Stelle werden den Attributen keine konkreten Werte zugewiesen. Das bedeutet man kann nicht sagen „x wird der Wert 3 zugewiesen“ sondern man sagt „x muss gleich 3 sein“ oder „x muss größer 5 sein“.

Diese Konzepte würden theoretisch schon ausreichen um die meisten Anforderungen einer Akzeptanztest-DSL zu erfüllen. Dem Benutzer eine Möglichkeit zur Definition von Instanzen zu geben wäre nicht unbedingt nötig. Möchte man eine bestimmte Instanz beschreiben, so muss man ein neues Domänenobjekt erstellen, welches diese ausreichend genau enschränkt. Gut eignet sich dazu z.B. die Verwendung von Eigenschaften mit Unique-Contraints bei der Formulierung der Einschränkungen (z.B. Nickname muss „Musterman“ sein). Die Vorbedingungen können bis zu einem gewissen Grad auch automatisch hergestellt werden. Wenn eine Vorbedienung z.B. besagt, dass ein Testbenutzer existieren muss und das nicht der Fall ist, dann könnte die Software selbstständig eine Instanz erzeugen, welche alle Einschränkungen eines Testbenutzers erfüllt. Nach der Testausführung könnte diese Instanz wieder gelöscht werden. Somit kann Testhygiene automatisiert stattfinden.

In der Praxis existieren allerdings sehr komplexe Datenbanken. Oft hat man nur eingeschränkte Schreibrechte. Eventuell möchte man auch eine bestehende Instanz in der Datenbank verwenden und bei dieser nur einzelne Eigenschaften ändern. Aus diesem Grund werden für Domänenobjekte die Funktionen create und find definiert, die eine neue Instanz in der Datenbank erzeugen bzw. eine bestehende Instanz aus der Datenbank sucht. Diese Instanzen können an Variablen gebunden werden. Anders als bei Domänenobjekten, können den Eigenschaften der Instanzen Werte zugewiesen werden. Bei diesen Zuweisungen werden die entsprechenden Spalten in der Datenbank aktualisiert. Instanzen haben die Funktion delete um sie aus der Datenbank zu löschen.

Damit ist die Akzeptanztest-DSL objektorientiert. Groovy bietet bereits ein Konzept für Objektorientierung. Wenn man dieses verwenden würde, müsste man es um die Möglichkeit erweitern, Einschränkungen für Attribute definieren zu können. Man bräuchte weitere Konstrukte um definieren zu können, wie sich Eigenschaften zusammen setzen. Beispielsweise würde folgendes nicht funktionieren:

class Benutzer extends smf_members {
  def Nickname = super.member_name
}

Würde man eine Instanz von der Klasse Benutzer erzeugen und dem Attribut Nickname einen Wert zuweisen wollen:

def testbenutzer = new Benutzer()
testbenutzer.Nickname = "Testbenutzer"

würde man die Information, wie sich die Eigenschaft Nickname zusammensetzt, überschreiben. Die Erzeugung von Instanzen inline-definierter Klassen, lässt sich für einen Nicht-Javaprogrammierer schwer lesen:

def poweruser = new Benutzer() {
  def where = {
    super.Beiträge.count() > 500
  }
}

Es wäre also viel technischer Overhead notwendig, wenn man das Klassenkonzept von Groovy verwenden und erweitern würde. Aus diesem Grund wird das Konzept der Domänenobjekte verwendet und um die Möglichkeit zur Erzeugung von Instanzen erweitert.

Dienstag, 29. März 2011

Die Beispieldomäne Forum

Um die Anforderungen an eine DSL für die Beschreibung von Akzeptanztests besser erfassen zu können, ist eine Beispieldomäne notwendig. Sebastian Teichgräber hat dazu eine Domäne aus der Beherbergungsbranche verwendet. Ich verwende als Beispieldomäne ein Online-Forum. Foren sind den meisten Internetbenutzern bekannt. Somit sind Anwendungsfälle in dieser Domäne leicht nachvollziehbar. Ein weiterer Vorteil ist, dass funktionierende und kostenlos verfügbare Forensoftware mit einigermaßen komplexen Datenbankstrukturen bereits existiert. Diese kann man verwenden, um die entwickelte DSL zu testen.

Online-Forum SMF


Ich verwende die Software Simple Machines Forum (kurz SMF) in Version 2.0 RC5. Bei der Installation wurde smf_ als Präfix für Tabellennamen angegeben. Somit heben sich die Tabellennamen besser von den Namen der Domänenobjekten ab, die manuell definiert werden. Die Software erzeugt bei der Erstellung 63 Tabellen. Somit macht es auch Sinn, von der Datenbankstruktur hin zum fachlichen Domänenmodell zu abstrahieren.

Anwendungsfälle in einem Online-Forum


Ein Online-Forum hat u.A. die folgenden Anwendungsfälle:



Der Anwendungsfall "Thema starten" sieht im Detail folgendermaßen aus:

Name Thema starten
Beschreibung Der Benutzer startet ein neues Thema.
Auslöser Der Benutzer möchte ein neues Thema starten.
Vorbedingungen
  • Der Benutzer ist angemeldet.
  • Das Unterforum, in dem das Thema erstellt werden soll, existiert.
  • Das zu erstellende Thema existiert in dem Unterforum noch nicht, in dem es erstellt werden soll.
Nachbedingung
  • Das zu erstellende Thema existiert im passenden Unterforum.
  • Das zu erstellende Thema hat einen Beitrag.
Standardablauf
  1. Der Benutzer wählt das Unterforum, in dem er ein Thema erstellen möchte.
  2. Der Benutzer wählt "Neues Thema".
  3. Der Benutzer gibt den Titel und den Text des Themas ein.
  4. Der Benutzer klickt auf senden.

Groovy-DSL für die Vor- und Nachbedingungen


Eine DSL für die Formulierung von Vor- und Nachbedingungen sollte so gestaltet sein, dass sie der natürlichen Sprache, die im letzten Abschnitt zur Formulierung verwendet wurde, möglichst ähnlich ist. Die Vor- und Nachbedingungen aus dem letzten Abschnitt können folgendermaßen als gültiger Groovy-Code dargestellt werden:

//Vorbedingungen
prüfe {
    Testbenutzer.istAngemeldet
    Testforum.existiert
    NeuesThema.existiertNicht
}

//Nachbedingungen
prüfe {
    NeuesThema.existiert
    Testforum.Themen.enthält NeuesThema
    NeuesThema.Beiträge.anzahl = 1
    NeuesThema.Beiträge.enthält NeuerBeitrag
}

Definition von Testdaten


Die Begriffe existiert, existiertNicht, anzahl und enthält sind eingebaute Bestandteile der DSL, da sie für die meisten Vor- und Nachbedingungen gebraucht werden. Die Begriffe Testbenutzer, Testforum, NeuesThema, NeuerBeitrag und istAngemeldet müssen vom Benutzer definiert werden, da sie domänenspezifisch sind. Die Definitionen sehen folgendermaßen aus:

definiere Testbenutzer {
    istEin Benutzer
    mit {
        Nickname = "Testbenutzer"
    }
}

definiere Testforum {
    istEin Forum
    mit {
        Name = "Testforum"
    }
}

definiere NeuesThema {
    istEin Thema
    mit {
        Titel = "Testthema"
    }
}

definiere NeuerBeitrag {
    istEin Beitrag
    mit {
        Titel = "Testthema"
        Text = "Testtext"
    }
}

Das Schlüsselwort definiere erzeugt ein neues Domänenobjekt in der Domäne. Mit istEin legt man fest auf welchen Domänenobjekt das zu erstellende basieren soll. Dabei werden alle Eigenschaften des Basisobjektes vererbt und sind im neuen verfügbar. Mit dem Schlüsselwort mit kann man Constraints für das neue Domainobjekt festlegen. Im Beispiel NeuerBeitrag wird z.B. festgelegt, dass der Titel "Testthema" und der Text "Testtext" sein muss.

Definition von Domänenobjekten


Was Benutzer, Forum, Thema und Beitrag sind muss ebenfalls definiert werden. Das ist je nach verwendeter Foren-Software anders. Bei dem verwendeten SMF-Forum kann man einen Beitrag beispielsweise folgendermaßen definieren:

definiere Beitrag {
    istEin smf_messages
    definiereEigenschaft Titel {
        istEigenschaft smf_messages.subject
    }
    definiereEigenschaft Text {
        istEigenschaft smf_messages.body
    }
}

Mit dem Schlüsselwort definiereEigenschaft legt man neue Eigenschaften für das Domänenobjekt fest. Diese können später z.B. in Constraints verwendet werden. In den meisten Fällen basiert die neue Eigenschaft auf vererbten Eigenschaft des Basisobjektes. Das wird durch das Schlüsselwort istEigenschaft festgelegt.

smf_messages ist eine Datenbanktabelle. subject und body sind Spalten dieser Tabelle. In der zu erstellenden Software ist smf_messages ebenfalls ein Domänenobjekt. Dieses Objekt wird von Software automatisch erstellt. Die Software erstellt für jede Tabelle mit all ihren Spalten jeweils genau ein Domänenobjekt. Diese Domänenobjekte bilden die Basis für alle benutzerdefinierten Domänenobjekte. Dadurch wird es möglich SQL-Queries zu generieren und die Vor- und Nachbedingungen überprüfen zu lassen.

Dienstag, 15. März 2011

Wie die Erstellung von Akzeptanztests in der Software ablaufen soll

Mit der fertigen Software soll es später mögliche sein, Akzeptanztests für datenbankbasierte Anwendungen zu erstellen und zu verwalten. Das Erstellen von Akzeptanztests soll dabei mithilfe von DSLs geschehen. Die Erstellung besteht aus den folgenden Schritten:

  • Beschreibung der Domäne
  • Beschreibung des Ausgangszustandes
  • Beschreibung wie die zu testende Software ausgeführt wird
  • Beschreibung des Endzustandes

Beschreibung der Domäne

In der Software soll es möglich sein, ein Domänenmodell zu beschreiben. Dieses Modell besteht aus Domänenobjekten. Die Domänenobjekte können z.B. Geschäftsobjekte, wie "Kunde" oder "Rechnung" sein. Die Domänenobjekte können dann verwendet werden, um den Ausgangs- und den Endzustand bei Testausführung zu beschreiben. In dem Domänenmodell wird beschrieben, welche Beziehung ein Domänenobjekt zu den Tabellen und Tabellenspalten der Datenbank hat. Dadurch wird es für die Software möglich, die Zustandsbeschreibungen mit Fachbegriffen aus der Domain in SQL-Queries zu übersetzen, diese auszuführen und das Ergebnis zurück zu liefern.

Um nicht bei null mit der Beschreibung des Domänenmodells anfangen zu müssen, kann dieses zu Beginn aus einer bereits vorhandenen Datenbank generiert werden. Dabei entspricht eine Tabelle der Datenbank mit all ihren Spalten genau einem Domänenobjekt. Dieses generierte Domänenmodell kann anschließend weiter bearbeitet werden, um es z.B. von technischen Details zu befreien (z.B. von "id"-Spalten).

Zum Anzeigen und Bearbeiten des Domänenmodells eignet sich wahrscheinlich eine grafische DSL am besten. Gerade bei großen Domänen mit sehr vielen Domänenobjekten ist es wahrscheinlich schwer den Überblick zu bewahren, wenn diese in einer textuellen DSL beschrieben wäre. Diese grafische DSL könnte sich vom Erscheinungsbild her an UML-Klassendiagrammen orientieren.

Beschreibung des Ausgangs- und Endzustandes

Bei der Beschreibung des Ausgangs- und des Endzustandes werden konkrete Instanzen einzelner Domänenobjekte beschrieben (z.B. Instanz "x" ist ein Domänenobjekt "Kunde" mit den Werten "Name" = "Mustermann" und "Vorname" = "Max"). In der Regel müssen nur wenige Instanzen beschrieben werden. Dadurch bleibt der Überblick auch bei der Beschreibung durch eine textuelle DSL erhalten. Im Idealfall können bei einer textuellen DSL die Vor- und Nachbedingungen aus den Use-Cases möglichst ähnlich übernommen werden.

Für die Beschreibung des Ausgangs- und des Endzustandes kann die gleiche DSL verwendet werden. Diese DSL hätte die grundlegende Form "Es existiert 20 von X". Bei der Auswertung der Beschreibung des Endzustandes wird überprüft, ob diese Aussage wahr ist. Ist sie es, so war der Test erfolgreich.
Bei der Auswertung der Beschreibung des Ausgangszustandes wird ebenfalls überprüft ob die Aussage wahr ist. Ist sie es, so kann mit der Ausführung der zu testenden Software begonnen werden. Ist sie es nicht, so kann von der Software angeboten werden den Ausgangszustand herzustellen.

Beschreibung wie die Software ausgeführt wird

Um den Akzeptanztest durchführen zu können, muss angegeben werden, wie die zu testende Software auszuführen ist. Hierfür ist wohl ein grafischer Dialog am besten geeignet. Dieser Dialog könnte verschiedene Möglichkeiten zur Ausführung der Software bieten, z.B.:

  • Ausführung von Javacode: So hat man z.B. die Möglichkeit eine zu testende Klasse zu instanziieren und auszuführen.
  • Ausführung von ausführbaren Dateien und Jar-Archiven mit Angabe von Paramatern
  • Manuelle Ausführung: So kann man z.B. eine Aktion über das Webinterface der Software per Hand ausführen.

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

Dienstag, 8. März 2011

Interne DSL mit Groovy Teil 1

Die zweite Sprache, die ich auf Eignung für interne DSLs testen will, ist Groovy. Groovy ist eine dynamische Sprache. Sie läuft ebenfalls in der Java VM. Die Syntax ist sehr ähnlich zu Java und damit wesentlich umfangreicher als die Syntax von Clojure. Dieses mehr an Syntax bedeutet gleichzeitig, dass mehr Techniken nötig sind um die Hostsprache Groovy so zu verbiegen, dass sie das "Look&Feel" einer DSL hat. In diesem ersten Teil werde ich einige Techniken von Groovy vorstellen, die zur Erstellung von internen DSLs genutzt werden können. Im zweiten Teil werde ich dann die Fragebogen DSL aus dem Clojure Beitrag mit Groovy umsetzen.

Techniken für die Erstellung von DSLs

Einsparung von Semikolons und Klammern

In Groovy sind Semikolons in den meisten Fällen optional. Klammern bei Funktionsaufrufen können auf der obersten Ebene ebenfalls entfallen.

//In Java und Groovy gültig
println("Test");
println("Hello"); println("World");
println(Integer.parseInt("3"));

println "Test"                                  //funktioniert
println "Hello" println "World"    //funktioniert nicht
println "Hello"; println "World"  //funktioniert
println Integer.parseInt "3"      //funktioniert nicht
println Integer.parseInt("3")     //funktioniert


Neue Methoden und Eigenschaften für Klassen zur Laufzeit hinzufügen

In Groovy ist es möglich jeder existierenden Klasse neue Methoden und Eigenschaften zur Laufzeit hinzuzufügen, ohne die Klassen neu kompilieren zu müssen. Dazu wird der ExpandoMetaClass-Mechanismus verwendet. Damit besteht z.B. die Möglichkeit Einheiten an Zahlen anhängen zu können (z.B. "1.km" oder "1.s") und entsprechende Umwandlungen durchzuführen, wie im folgenden Beispiel zu sehen ist.

//Vererbung von neuen Methoden und Eigenschaften welche über
//den ExpandoMetaClass-Mechanismus hinzugefügt werden ist
//standardmäßig deaktiviert. Mit der folgenden Zeile wird
//Vererbung aktiviert.
ExpandoMetaClass.enableGlobally()

Number.metaClass.getProperty = {String einheit ->
    switch(einheit) {
        //delegate ist die Instanz für welche die Eigenschaft
        //abgerufen wird.
        case 'km': return delegate * 1000
        case 'm': return delegate
        case 'cm': return delegate * 0.01
        case 'mm': return delegate * 0.001
        default: 'Die Einheit ' + einheit + ' ist unbekannt.'
    }
}

println(1.km)
println(10.cm)
println(20.ha)

Bei Ausführung des Skriptes entsteht folgende Ausgabe:

1000
0.10
Die Einheit ha ist unbekannt.

Mehr zu dem Thema kann man hier nachlesen.

Abfangen nicht definierter Variablen

In jedem Groovy-Skript existiert eine Instanz der Klasse "groovy.lang.Binding". Diese Instanz löst alle nicht im Skript definierten Variablen auf. Dieser Mechanismus wird u.a. genutzt um Informationen zwischen dem Groovy Skript und der aufrufenden Sprache auszutauschen. Die aktuelle Instanz ist über die Variable "binding" erreichbar. Ersetzt man diese aktuelle Instanz mit einer Instanz einer eigenen Klasse welche von "groovy.lang.Binding" erbt, kann man das Verhalten des Skriptes bei nicht definierten Variablen selber festlegen. Das folgende Beispiel demonstriert diese Möglichkeit.

class TestBinding extends Binding {
    def getVariable(String name) {
      println("Variable nicht gefunden")
      return null
    }
}

binding = new TestBinding()

nichtDefinierteVariable

Die Ausgabe bei Ausführung des Skriptes ist "Variable nicht gefunden", der Rückgabewert ist null.

Überladen von Operatoren

Ähnlich wie in C++ können Operatoren in Groovy überladen werden. Jeder Operator wird durch eine Methode repräsentiert, welche man in der eigenen Klasse einfach implementieren muss, um den entsprechenden Operator zu überschreiben. Eine Liste mit den Methodennamen findet man hier.

class Kaffe {
    List zutaten = []
    
    def leftShift(String zutat) {
        this.zutaten << zutat
    }
    
    def inhalt() {
        println("Im Kaffe ist: ")
        zutaten.each {it ->  print("* "); println(it)}
    }
}

kaffe = new Kaffe()
kaffe << "Wasser" 
kaffe << "gemahlene Bohnen" 
kaffe << "Milch"
kaffe.inhalt();

Ergibt die Ausgabe

Im Kaffe ist: 
* Wasser
* gemahlene Bohnen
* Milch

Methodenzeiger

Es ist möglich Zeiger auf Methoden einer bestimmten Instanz einer Klasse zu erstellen.

class Pflanze {
    def int wassermenge
    def gießen(int menge) {
        wassermenge += menge
    }
}

kaktus = new Pflanze();
def gieße = kaktus.&gießen

gieße 300
gieße 100

Links

Freitag, 4. März 2011

Interne DSL mit Clojure

Clojure ist ein Lisp Dialekt. Es läuft in der Java VM. Ein ausführliches Tutorial zu der Sprache findet man hier. Fast alles in Clojure lässt sich in der folgenden Form darstellen:

([Operation/Funktion] <Parameter 1> ... <Parameter n>)

Es gibt weitere sprachliche Konstruktionen. Die meisten sind allerdings "Syntaktischer Zucker" um den Code lesbarer zu machen. Durch die einfache Syntax scheint Clojure sehr geeignet für interne DSLs zu sein. Der Unterschied zwischen API und DSL ist in Clojure nicht sehr groß.

Installation


Clojure besteht im Wesentlichen aus einer JAR-Datei. Diese kann man sich auf der Projektseite herunterladen. Starten lässt sich der Interpreter dann wie folgt:

java -cp C:\Pfad\zu\clojure.jar clojure.main

Um nicht jedes Mal die ganze Zeile wie oben eingeben zu müssen, lohnt es sich ein Startskript zu erstellen. Unter Windows kann das z.B. ein Batch-Skript mit folgendem Inhalt sein.

@ECHO OFF
java -cp C:\Pfad\zu\clojure.jar clojure.main  %*

@ECHO OFF sorgt dafür, dass bei der Ausführung der folgenden Befehle die Kommandozeile nicht mit ausgegeben wird. Das ist wichtig wenn die Ausgabe eines Clojure Skriptes beispielsweise HTML ergibt und in eine Datei umgeleitet werden soll. %* steht für alle Argumente die dem Batch-Skript übergeben wurden. In diesem Beispiel werden also alle Argumente an den Clojure Interpreter übergeben.

Die Fragebogen-DSL


Einen Fragebogen direkt in HTML-Code zu erzeugen bedeutet viel Schreibarbeit, da viel Overhead erstellt werden muss damit gültiges HTML entsteht. Eine Kfz-Umfrage könnte folgendermaßen aussehen:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <title>Kfz-Umfrage</title>
  </head>
  <body>
    <h1>Kfz-Umfrage</h1>
    <form action="">
      <h2>Welche Marke hat ihr Fahrzeug?</h2>
      <input type="radio" value="Opel" name="Welche Marke hat ihr Fahrzeug?">Opel<br /‌>
      <input type="radio" value="Renault" name="Welche Marke hat ihr Fahrzeug?">Renault<br /‌>
      <input type="radio" value="BMW" name="Welche Marke hat ihr Fahrzeug?">BMW<br /‌>

      <h2>Wie lautet die Modelbezeichnung?</h2>
      <input type="text" name="Wie lautet die Modelbezeichnung?"<br /‌>

      <h2>Welche Ausstattungsmerkmale hat ihr Fahrzeug?</h2>
      <input type="checkbox" value="Klimaanlage" name="Welche Ausstattungsmerkmale hat ihr Fahrzeug?">Klimaanlage<br /‌>
      <input type="checkbox" value="Navigationssystem" name="Welche Ausstattungsmerkmale hat ihr Fahrzeug?">Navigationssystem<br /‌>
      <input type="checkbox" value="ESP" name="Welche Ausstattungsmerkmale hat ihr Fahrzeug?">ESP<br /‌>

      <br /‌>
      <input type="submit" value="Senden">
    </form>
  </body>
</html>

Der größte Teil des Codes kann in jedem Fragebogen verwendet werden und so liegt es nahe diesen zu generieren. Unterscheiden lassen sich solche Fragebögen an den folgenden Dingen:

  • Titel des Fragebogens
  • Fragen des Fragebogens mit ihrem Typ (Textfrage, Einfache Auswahl, Mehrfache Auswahl)
  • Antwortmöglichkeiten zu den einzelnen Fragen

Mit einer entsprechenden internen DSL in Clojure kann man den Fragebogen von oben in der folgenden Form beschreiben.

(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")))

Die verwendeten Sprachkonstrukte wie "Fragebogen" oder "Frage" müssen jetzt nur noch definiert werden.

(def doctype "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">
")

(defn Antwort [text]
  text)

(def Einfachauswahl {:Typ :Auswahl, :html "radio"})
(def Mehrfachauswahl {:Typ :Auswahl, :html "checkbox"})
(def Texteingabe {:Typ :Text})

(defmulti Frage (fn [typ frage & antworten] (:Typ typ)))

(defmethod Frage :Auswahl [typ frage & antworten]
  (loop [text (str "<h2>" frage "</h2>"), antworten antworten]
    (if (empty? antworten)
      text
      (recur
        (str
          text
          "<input type=\"" (:html typ) "\""
          " value=\"" (first antworten) "\""
          " name=\"" frage "\" />"
          (first antworten) "<br /‌>")
        (rest antworten)))))

(defmethod Frage :Text [typ, text]
  (str "<h2>" text "</h2>"
    "<input type=\"text\" name=\"" text "\" /><br /‌>"))

(defn Fragebogen [text & fragen]
  (println (str
    (reduce
      str (str
            doctype
            "<html><head><title>"
            text
            "</title></head><body><h1>"
            text
            "</h1><form action=\"\">")
      fragen)
    "<br /‌><input type=\"submit\" value=\"Senden\" /></form></body></html>")))

Speichert man die Definition der DSL in die Datei "Fragebogen.clj" und den Kfz-Fragebogen in die Datei "Kfz-Fragebogen.clj", kann man mit Hilfe der weiter oben angegebenen Batchdatei die HTML-Version des Fragebogens auf die folgende Weiße erzeugen.

clj.bat -i Fragebogen.clj Kfz-Fragebogen.clj > Kfz-Fragebogen.html

Das Symbol ">" sorgt dafür das die Ausgabe des Befehls in die Datei Kfz-Fragebogen.html umgeleitet wird. Nach der Ausführung erhält man den folgenden Fragebogen: