Donnerstag, 22. Februar 2007

Dieser Blog ist tot. Ich blogge weiter auf dem «Agile Trail».

Kilometerfresser

Was passiert, wenn ein Läufer sich aus einer Groovy Console, Inlinetests, Groovy Beans, der Meta-Objekt-Programmierung, Swing, GStrings, Regulären Ausdrücken und Closures einen Kilometerfresser baut? Es gibt einen kurzen Einblick in die Welt von Groovy...

Ich bin Läufer und führe ein Trainingstagebuch. Dort trage ich ein, wieviele Kilometer ich am Tag gelaufen bin. In welchem Tempo, wieviele Kalorien ich verbrannt habe, welches Paar Schuhe ich benutzt habe, meinen Durchschnittspuls, und noch ein paar Kleinigkeiten mehr, aber hauptsächlich eben die Kilometer. Wieviele Kilometer am Ende der Woche dann zusammen gekommen sind, gibt mir u. a. einen Hinweis darauf, wie gut ich noch im Plan bin.

Aber die Rechnerei ist oft nervig. Da tippt man sich etwas zusammen im Taschenrechner wie

2 * 2 + 6 * 0.3 + 5 * 0.1 + 14.2 ...
usw. Das liest sich dann so: 2 mal 2 km (fürs Ein- und Auslaufen) + 6 mal 300 m Sprint + 5 mal 100 m Trab + 14,2 km Wettkampf. Und das sind noch nichtmal zwei Tage der letzten Woche gewesen. Auch vertippe ich mich des öfteren und muss dann alles nochmal neu eintippen.

Grund genug, mit Groovy zu spielen! Die Groovy Console bietet schon die Möglichkeit, einen Ausdruck wie oben beschrieben genau so einzugeben und dann auswerten zu lassen. Da kommt dann 20.5 raus. Soweit, so gut. Aber lesbar ist diese Eingabe nicht. Außerdem muss man immer alles in km oder m angeben, aber kann nicht mischen, will man nicht immer umrechnen müssen. Schöner wäre zu lesen:
2 * 2.km + 6 * 300.m + 5 * 100.m + 14.2.km
In Groovy ist Alles ein Objekt, also auch ein Ausdruck wie 14.2. Es gilt
assert BigDecimal.class == 14.2.class
Dieser Inlinetest sichert zu, dass 14.2 vom Typ BigDecimal ist. Objekte können Methoden haben, Getter sind Methoden, also können Objekte Getter haben. Groovy-Getter sind da noch etwas Besonderes dank der Groovy Beans. Wenn ein Groovy-Objekt einen Getter hat, dann kann man so drauf zugreifen
14.2.getKm()
, also wie in Java, aber man kann den Groovyweg gehen und so drauf zugreifen
14.2.km
Gut, soweit die Theorie. Wie bekommt nun ein BigDecimal einen km-Getter? Groovys Meta-Objekt-Programmierung kann uns hier weiterhelfen. Zur Laufzeit können damit Klassen und Objekte um Konstruktoren, Methoden und Variablen ergänzt werden, also auch um Getter. Die einfachste Variante, in Groovy dies zu tun, bietet eine Category. Das ist, einfach gesprochen, eine Klasse, in der wir einem Objekt neue Dinge beibringen können.
class StreckenCategory {
static def getKm(selbst) {
selbst
}
}
In einer Category definieren die statischen Methoden das neue Verhalten, hier die Methode getKM(). Der Parameter selbst gibt hier an, auf welche Objekte das neue Verhalten anzuwenden ist. Wenn selbst vom Typ String wäre, so würden ab sofort nur Strings solch eine Methode haben. Wenn kein Typ angegeben ist, dann ist der Parameter vom Typ Object. Alle Objekte, auf die die Category angewendet werden, bekommen dann eine Methode getKm() Auf BigDecimal will ich den Parameter hier nicht beschränken, da auch die 2 im Eingangs erwähnten Beispiel mit neuer Methode ausgerüstet werden soll, und 2 ist vom Typ Integer.

Eine zweite Methode ist in dieser Category noch erforderlich, denn 0.3.m sollen ja schließlich auch noch berücksichtigt werden.
static def getM(selbst) {
selbst / 1000
}
Um einer Category mitzuteilen, welche Objekte sie beeinflussen soll, gibt es in Groovy das Schlüsselwort use. Der folgende Code demonstriert seine Anwendung:
use(StreckenCategory){
assert 20.5 ==
2 * 2.km + 6 * 300.m + 5 * 100.m + 14.2.km
}
So, an diesem Punkt wäre ich eigentlich schon fertig. Aber das reicht mir noch nicht so ganz, denn ich will ja nicht ständig mit der Category in der Groovy Console hantieren müssen, wenn ich mal ein paar Strecken berechne. Eine kleine Oberfläche wäre hier wohl ganz nett.

Groovy bietet für Oberflächen den SwingBuilder. Builder sind Klassen, mit denen sich elegant hierarchische Strukturen darstellen lassen. Mit dem SwingBuilder kann ich eine Swingoberfläche mit weitaus geringerem Aufwand erstellen als in Java.
import groovy.swing.SwingBuilder

def swing = new SwingBuilder()
def frame =
swing.frame(title:'Kilometerfresser') {
textField(columns:100, actionPerformed: {
event ->
use (StreckenCategory.class) {
event.source.setText(
werteAus(event.source.text)
)
}
})
}
frame.pack()
frame.show()
Was passiert hier? Zuerst erzeuge ich mir einen SwingBuilder. Mit dem baue ich dann ein JFrame mit dem Titel Kilometerfresser und einem Textfeld. Das Textfeld soll 100 Zeichen fassen können (ich laufe viel in der Woche). Wenn eine Aktion ausgelöst wird (actionPerformed), dann wird ein event ausgelöst. Dieses Event kann dann nach dem Inhalt des Textfeldes gefragt werden (event.source.text - ein Getter!). Die Methode werteAus(...) beschreibe ich gleich.


Ohne jetzt näher auf den SwingBuilder einzugehen erkennt man gut das Prinzip dahinter. Führt man den Code aus, poppt ein Fenster auf. Im Fenster ist ein Textfeld enthalten, in welches ich meine Kilometerberechnungen wie beschrieben eintippen kann. Die Entertaste löst die Aktion aus, meine Berechnungen werden ausgeführt und ich erhalte einen neue Kilometerangabe, und zwar dank der folgenden Methode:
def werteAus(string) {
def km = evaluate(string)
"${km}.km"
}
Die Methode ist kurz, aber hat's in sich: Evaluate(...) wertet mir beliebigen Groovycode aus. Die letzte Zeile macht vom impliziten Returnwert gebrauch (in Java würde da stehen return "${km}.km"). Und schließlich helfen hier die GStrings, eine Abwandlung der in Java bekannten Strings. In einem GString kann ich durch die Dollar-Schreibweise auf Variablen zugreifen, wie hier geschehen mit der Variablen km.

Aber so ganz toll ist das ja immer noch nicht, oder? Wer schreibt schon 14.2.km im deutschsprachigen Raum? 14.2 ist eher aus US/UK und .km schreibt man eigentlich mit Leerzeichen statt mit Punkt. Eigentlich möchte ich schreiben können:
2 * 2 km + 6 * 300 m + 5 * 100 m + 14,2 km
Hier kommen nun ein wenig Reguläre Ausdrücke ins Spiel, die von Groovy in besonderer Art und Weise unterstützt werden, wobei die Notation in Java und Groovy gleich ist. Die Methode parse(string) soll Kommata sowie Leerzeichen zwischen Zahl und Maßeinheit durch Punkte ersetzen. Die Methode formatiere(string) soll dagegen alle Dezimalpunkte durch Dezimalkommata ersetzen:
def parse(string) {
string
.replaceAll(/(\d+),(\d+)/, {
alle, km, m -> "${km}.${m}"
})
.replaceAll(/ (k*m)/, {
alle, km -> ".${km}"
})
}

def formatiere(string) {
string.replaceAll(/(\d+).(\d+)/, {
alle, km, m -> "${km},${m}"
})
}
Zum Einsatz kommt hier die Methode replaceAll(regexp, closure), die ein wenig anders funktioniert als die gleichnamige Methode an Java-Strings. Der erste Parameter ist der Reguläre Ausdruck, der zweite hingegen eine Closure. Eine Closure ist ein ausführbarer Codeblock und man kann ihn sich wie eine anonyme Klasse vorstellen. In Groovy-Closures sind das, was vor dem Pfeil (->) steht, die Variablen, die zum Einsatz kommen können. Jede Variable stellt hier eine Gruppenreferenz des Regulären Ausdrucks dar - also das, was man mit den Klammern "gefangen" hat. /(\d+).(\d+)/ stellt also drei Gruppen dar: die erste Klammerung (\d+) beschreibt eine oder mehrere Zahlen, die zweite, weil mit der ersten identisch, ebenfalls. Die erste Gruppe stellt also die Kilometer dar, die zweite die Meter. Implizit gibt es immer die Gruppe 0, die alles abdeckt, was der Reguläre Ausdruck beschreibt. Somit können wir in der Closure drei Variablen angeben: alle für die Gesamtübereinstimmung, km für die Kilometer und m für die Meter. Wir können alle nicht weglassen, da alle Gruppen der Closure übergeben werden, unabhängig davon, welche dann tatsächlich benutzt werden. Das, was nun nach dem Pfeil in der Closure steht, ist das, was anstelle der Gesamtübereinstimmung des Regulären Ausdrucks ersetzt wird; aus 14.2 wird also 14,2.

Die beiden Methoden zum Parsen und formatieren baue ich in die Methode zum Auswerten ein:
def werteAus(string) {
def km =
evaluate(parse(string)) as String
def formatierteKm = formatiere(km)
"${formatierteKm} km"
}

Fertig. Nun reicht's aber auch, sonst programmiere ich hier noch mein komplettes Lauftagebuch :-) Der Code kann bei ByteMyCode in Gänze und Farbe betrachtet werden und ist lauffähig mit Groovy 1.0 in der Groovy Console. Viel Spaß damit!

blog comments powered by Disqus