Groovy: Builder
← Closures | ↑ Neue Konzepte | → Templates
Hierarchische Baumstrukturen sind Dinge, die in der Programmierung immer wieder vorkommen. Geläufige Beispiele hierfür sind die Strukturen von XML-Dokumenten und die innere Anordnung der Elemente grafischer Benutzeroberflächen. Erstaunlicherweise bieten die traditionellen Programmiersprachen wenig systematische Unterstützung dafür, so dass solche Strukturen immer wieder neu entwickelt werden und mit den verfügbaren Mitteln nur umständlich bearbeitet werden können. Mit Groovy können Sie hierarchische Objektstrukturen in einer quasi-deklarativen Weise aufzubauen. Möglich wird dies durch die dynamischen Eigenschaften der Sprache, durch die Methodenaufrufen und -Argumenten sowie den Closures neue Bedeutungen verliehen werden können.
Das Prinzip
BearbeitenEine hierarchische Struktur, wie wir sie hier meinen, besteht aus Knotenelementen, die jeweils beliebig viele untergeordnete Elemente aber nur ein übergeordnetes Element haben können. Genau ein Element, nämlich das Wurzelelement, hat kein übergeordnetes Element. Jedes Element hat einen Namen und null oder mehrere benannte Attribute, die es beschreiben. Genau dies lässt sich nun akkurat mit Hilfe der Groovy-Syntax abbilden:
- Der Methodenname ist der Name eines Knotens
- Die benannten Argumente (als Map) definieren die Attribute des Knotens
- Ein Closure-Argument bildet eine neue Ebene für untergeordnete Knoten
Dabei muss der Methodenname natürlich immer vorhanden sein, aber die benannten Argumente und die Closure können auch wegfallen, wenn sie nicht benötigt werden.
Groovy kennt nun eine spezielle Art von Klassen, die als Builder bezeichnet werden, die alle Methodenaufrufe, die in dieses Muster fallen, nicht wirklich als Aufrufe wirklich existierender oder virtueller Methoden interpretiert, sondern als Definitionen von Knotenelementen in einer Baumhierarchie. Was mit diesen Definitionen dann geschieht, hängt von der jeweiligen Implementierung des Builders ab.
Ein einfaches, konkretes Beispiel für einen solchen Builder ist die Klasse groovy.util.NodeBuilder. Sie baut aus den Knotendefinitionen ein Netz von Objekten der Klasse groovy.util.Node, einer Mehrzweck-Klasse in der Groovy-Bibliothek, die zur Abbildung aller möglichen Baumstrukturen gedacht ist und auch unabhängig vom NodeBuilder einsetzbar ist. Ein Node-Objekt hat einen Namen, eine Referenz auf ein übergeordnetes Objekt, eine Map mit den Namen und Werten von Attributen sowie eine Liste mit untergeordneten Objekten. Mit Groovy kommen aber noch ein paar weitere nützliche Klassen, die auf dem Builder-Prinzip beruhen, z.B. ein SwingBuilder für den Aufbau von Anwendungsoberflächen und ein MarkupBuilder zum Erzeugen von XML- und HTML-Dokumenten. Im Kapitel ##LIBRARY## werden wir uns näher mit ihnen beschäftigen,
Beispiel Taxonomie
BearbeitenMit Hilfe des NodeBuilder wollen wir nun eine hierarchische Taxonomie der Wildkatzenarten anlegen. Dabei bilden Familie, Unterfamilie, Gattung und Art die Hierarchieebenen. Wir verwenden diese Hierarchieebenen als Elementnamen; alle Elemente haben ein zusätzliches Attribut name für den eigentlichen Namen, und auf der Ebene der Arten gibt es noch einige zusätzliche Attribute. Folgendes Beispiel zeigt einen Ausschnitt aus dieser Hierarchie als ein Groovy-Skript namens Wildkatzen.groovy.
builder = new NodeBuilder() familie = builder.familie(name:'Felidae') { unterfamilie(name:'Acinonychinae') { gattung(name:'Acinonyx') { art(name:'A. jubatus',bez:'Gepard',wiss:'Schreber',jahr:1775) } } unterfamilie(name:'Felinae') { gattung(name:'Caracal') { art(name:'C. caracal',bez:'Karakal',wiss:'Schreber',jahr:1776) } gattung(name:'Catopuma') { art(name:'C. badia',bez:'Borneo-Goldkatze',wiss:'Gray',jahr:1874) art(name:'C. temminickii', bez:'Asiat. Goldkatze',wiss:'Vigors&Horsfield',jahr:1827) } gattung(name:'Felis') { art(name:'F. bieti',bez:'Graukatze',wiss:'Milne-Edwards',jahr:1892) art(name:'F. chaus',bez:'Rohrkatze',wiss:'Schreber',jahr:1777) art(name:'F. margarita',bez:'Sandkatze',wiss:'Loche',jahr:1858) } } } new NodePrinter().print(familie)
Das Skript legt zuerst eine Instanz des NodeBuilder an und ruft dann bei ihm die scheinbare Methode famile() auf. Diesen Methodenaufruf fängt der NodeBuilder ab und legt stattdessen eine Node-Instanz mit dem Namen der Methode, also „familie“ an und setzt bei ihr ein Attribut „name“ mit dem Wert „Felidae“. An dieses Wurzelobjekt hängt der NodeBuilder nun nach und nach die untergeordneten Objekte, die durch die scheinbaren Methodenaufrufe in den verschachtelten Closures definiert werden.
Am Ende erhalten wir das Wurzelobjekt als Rückgabewert. Indem wir es an die print()-Methode einer Instanz der Klasse groovy.util.NodePrinter übergeben, können wir den ganzen Baum ordentlich formatiert auf der Konsole ausgeben. Wir verzichten hier auf die Wiedergabe des Ergebnisses – es sieht genau so aus wie der Programmcode, mit dem wir den Baum definiert haben.
Navigation im Baum
BearbeitenIn dem Ergebnis kann man jetzt mit GPath-Ausdrücken navigieren. Beispielsweise können Sie, wenn Sie am Ende des Skripts folgende Zeile hinzufügen, die deutschsprachigen Bezeichnungen aller verzeichneten Katzenarten ausgeben:
familie.unterfamilie.gattung.art.each { println it.'@bez' }
Die normale Property-Navigation (z.B. familie.unterfamilie) liefert immer den oder die Unterknoten mit dem betreffenden Namen. Wenn wir ein Attribut referenzieren wollen, müssen wir ein @-Zeichen vor dem Attributnamen einfügen, wie hier bei bez, und da dies keinen gültigen Groovy-Name ergibt, muss '@bez' dann auch noch in Anführungszeichen eingeschlossen werden. (Sie können nicht einfach it.bez schreiben, da Groovy sonst versuchen würde, auf ein Feld der Klasse Node mit dem Namen bez zuzugreifen, was ja hier nicht erwünscht ist.
Es gibt noch ein paar weitere Besonderheiten beim Zugriff auf die virtuellen Properties eines Node-Objekts, Folgende Tabelle enthält eine Zusammenfassung der entsprechenden Regeln. Dabei sei angenommen, dass die Variable n auf den Knoten für die Unterfamilie „Felinae“ verweist.
Form | Beispiel | Bedeutung |
---|---|---|
name | n.gattung | Liefert eine Liste mit allen direkt untergeordneten Knoten, deren Name gleich name ist. Im Beispiel sind es die drei Knoten für die Gattungen „Caracal“, „Catopuma“ und „Felis“. |
'@name' | n.'@name' | Liefert den Wert des Attributs mit dem Namen name. Im Beispiel ist das Ergebnis der String „Felinae“. Der Ausdruck n.name würde dagegen „unterfamilie“ liefern. |
'..' | n.'..' | Liefert das direkt übergeordnete Node-Objekt, oder null, wenn das aktuelle Element ein Wurzelknoten ist. Im Beispiel wäre das Ergebnis der Wurzelknoten. |
'*' | n.'*' | Liefert eine Liste mit allen direkt untergeordneten Knoten. Im Beispiel sind es die drei Knoten für die Gattungen „Caracal“, „Catopuma“ und „Felis“. |
'**' | n.'**' | Liefert rekursiv alle untergeordneten Knoten. Im Beispiel sind es die Knoten für „Caracal“, „C. caracal“ „Catopuma“, „C. badia“, C.temminickii“, „Felis“, „F. bieti, „F. chaus“ und „F. margarita“. |
Programmieren und Deklarieren
BearbeitenMomentan sind Sie vielleicht noch geneigt, zu sagen, dass Sie dies mit einer XML-Datei auch ganz gut hinbekommen hätten – wenn auch mit etwas mehr Schreibarbeit. Das Besondere an den Builder-Klassen in Groovy ist aber etwas anderes: Sie können hier den deklarativen Programmierstil mit normalem prozeduralen Teilen mischen und dadurch Teile der Struktur auch generieren oder in Subroutinen auslagern. Denn alle Groovy-Anweisungen, z.B. Schleifen, und alle Methoden, die dem nicht vom Builder-Objekt abgefangen werden, stehen Ihnen weiterhin für die normale prozedurale Programmierung zu Verfügung. Im obigen Beispiel stellen wir beispielsweise fest, dass das Schreiben der untersten Hierarchieebene relativ mühsam ist, weil immer wieder alle Feldnamen redundant angeben müssen. Also schreiben wir im Skript eine Methode, die uns die Arbeit abnimmt.
def art(name,bez,wiss,jahr) { builder.art(name:name,bez:bez,wiss:wiss,jahr:jahr) }
Die Methode art() hat vier Parameter, deren Werte Sie einfach als benannte Parameter ebenfalls durch einen art()-Aufruf, an die Builder-Instanz weitergibt. Nun können Sie diese Zeile
art(name:'F. margarita',bez:'Sandkatze',wiss:'Loche',jahr:1858)
ersetzen durch jene:
art('F. margarita','Sandkatze','Loche',1858)
und sich damit etwas Schreibarbeit ersparen.
Neben dem Ausgliedern von Teilen der Baum-Deklaration in getrennte Methoden sind typische Beispiele für die Mischung von deklarativer und prozeduraler Programmierung in den Builder-Objekten Schleifen für die Generierung von Teilbäumen aus irgendwelchen Daten oder die Berechnung der Attributwerte.
Eigene Builder programmieren
BearbeitenWie erwähnt, umfasst die Groovy-Bibliothek bereits eine ganze Reihe von Builder-Klassen; im Anhang Wichtige Klassen und Interfaces sind sie alle mit aufgeführt. Es ist aber auch nicht schwierig, selbst einen zu bauen. In diesem Beispiel wollen wir einen einfachen Builder programmieren, der ein formatiertes Strukturabbild der Baumstruktur direkt auf der Konsole ausgibt. Wir nennen ihn – etwas hochtrabend – PrettyPrintBuilder.
Wir brauchen eigentlich nichts weiter zu tun, als eine von groovy.util.BuilderSupport abgeleitete Klasse zu definieren, die folgende abstrakte Methoden implementiert.
- Object createNode(Object name)
- Object createNode(Object name, Map attributes)
- Jede dieser Methoden legt einen neuen Knoten an und liefert diesen als Ergebnis zurück. Dabei ist name der Name der scheinbar aufgerufenen Methode und damit zugleich der Knotenname. Die beiden Methoden unterscheiden sich nur darin, dass die eine noch zusätzlich Attribute als Map übergeben erhält.
- Object createNode(Object name, Object value)
- Object createNode(Object name, Map attributes, Object value)
- Diese beiden Methoden legen ebenfalls neue Knoten an, haben aber zusätzliches noch ein Argument, das typischerweise für Textknoten verwendet wird.
- void setParent(Object parent, Object child)
- Diese Methode können Sie implementieren, um einem Knoten einen übergeordneten Knoten zuzuorden.
- protected void nodeCompleted(Object parent, Object node)
- Wird aufgerufen, wenn die Bearbeitung eines Knotens (mit allen Unterknoten) beendet ist. Im gegensatz zu den anderen Methoden ist diese nicht abstrakt, muss also nicht zwingend überschrieben werden.
Eine hilfreiche Property des Node-Objekts, die Sie benutzen können, ist current. Sie verweist auf das aktuelle Knotenobjekt und ermöglicht Ihnen, eigene Verknüpfungen herzustellen.
Das folgende Beispiel zeigt die Quelldatei PrettyPrintBuilder.groovy komplett.
public class PrettyPrintBuilder extends BuilderSupport { protected createNode(name) { createNode(name,null,null) } protected createNode(name, value) { createNode(name,null,value) } protected createNode(name, Map attr) { createNode(name,attr,null); } protected createNode(name, Map attr, value) { def level = current ? current : 0 // Aktuelle Tiefe println "| " * level if (level) print "| " * (level-1) + '+-' if (name=='art') { print "$attr.name - $attr.bez ($attr.wiss, $attr.jahr)" } else { print "$attr.name (${name[0].toUpperCase()}${name[1..-1]})" } level+1 // Die neue Tiefe wird current-Objekt } protected void setParent(Object parent, Object child) {} }
Wie Sie sehen, erzeugen wir hier keine Node- und auch keine sonstigen Objekte, sondern schreiben einfach die Inhalte der Knoten optisch etwas aufbereitet aus. Die current-Property, die jeweils den aktuellen Knoten bezeichnet, benutzen wir einfach dazu, die Schachtelungstiefe mitzuführen.
Im Grunde implementieren wir hier nur die Methode createNode() mit drei Argumenten; die anderen createNode()-Methoden delegieren einfach an diese Methode. Und setParent() ist nur der Form halber implementiert, da sie in BuilderSupport abstrakt definiert ist.
Um unseren neuen Builder auszuprobieren, ändern wir unser obiges Skript Wildkatzen.groovy, so dass es diesen anstelle des NodeBuilder verwendet. Korrigieren Sie also die erste Zeile:
builder = new PrettyPrintBuilder()
Und entfernen Sie dann noch die letzte Zeile mit dem NodePrinter-Aufruf; den brauchen wir nicht mehr, da unser PrettyPrintBuilder ja selbst etwas ausgibt. Wenn wir es nun aufrufen, sieht es so aus:
> groovy Wildkatzen Felidae (Familie) | +-Acinonychinae (Unterfamilie) | | | +-Acinonyx (Gattung) | | | | | +-A. jubatus - Gepard (Schreber, 1775) | +-Felinae (Unterfamilie) | | | +-Caracal (Gattung) | | | | | +-C. caracal - Karakal (Schreber, 1776) | | | +-Catopuma (Gattung) | | | | | +-C. badia - Borneo-Goldkatze (Gray, 1874) | | | | | +-C. temminickii - Asiat. Goldkatze (Vigors&Horsfield, 1827) | | | +-Felis (Gattung) | | | | | +-F. bieti - Graukatze (Milne-Edwards, 1892) | | | | | +-F. chaus - Rohrkatze (Schreber, 1777) | | | | | +-F. margarita - Sandkatze (Loche, 1858)
Damit haben Sie ein sehr einfaches Beispiel eines Builder kennengelernt. Es soll deutlich machen, welche Möglichkeiten Ihnen mit einem Builder prinzipiell zur Verfügung stehen und wie einfach es ist, diese Art der semi-deklarativen Programmierung für eigene Zwecke zu nutzen.
← Closures | ↑ Neue Konzepte | → Templates