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.