Anhänge

Anhang A: Kurze Einführung in Groovy

Groovy ist eine Programmiersprache auf der JVM, die das Ziel verfolgt, sich perfekt in des Java-Ökosystem zu integrieren, den Übergang für Java-Entwickler nahtlos zu gestalten und trotzdem so viele neue Fähigkeiten mitzubringen, dass fast alles einfacher und manches überhaupt erst möglich wird.

Bad old Java

Fangen wir mit einer einfachen Java-Klasse an und verwandeln diese Schritt für Schritt in idiomatisches Groovy.

 1 public class HumanBeing {
 2     private String name;
 3 
 4     public String getName() {
 5         return name;
 6     }
 7 
 8     public void setName(String newName) {
 9         this.name = newName;
10     }
11 
12     public HumanBeing(String name) {
13         this.name = name;
14     }
15 
16     public String computeFromName(DoWithName code) {
17         return code.withName(name);
18     }
19 
20     public static void main(String[] args) {
21         HumanBeing son = new HumanBeing("Jannek");
22         son.setName("Niklas");
23         DoWithName code = new DoWithName() {
24             @Override
25             public String withName(String name) {
26                 return name + name;
27             }
28         };
29         System.out.println(son.computeFromName(code));
30     }
31 
32     interface DoWithName {
33         String withName(String name);
34     }
35 }

Diese Klasse erlaubt die Instanziierung menschlicher Wesen mit einer einzigen Property, welche den Konventionen für JavaBeans Zugriffsoperationen folgt.

Erster Schritt: *.java -> *.groovy

Im ersten Schritt unserer Metamorphose benennen wir einfach die Datei HumanBeing.java in HumanBeing.groovy um und benutzen für die Kompilierung groovyc statt javac. Dabei machen wir uns die Tatsache zu nutze, dass 99% allen Java-Codes auch gültigen Groovy-Code darstellt.

Doch Vorsicht, manchmal ändert sich bei dieser Umstellung die Semantik des Programms, z.B. weil Groovys Method-Dispatching normalerweise1 dynamisch anhand der Laufzeit-Typen der Parameter stattfindet. In unserem einfachen Beispiel ist dieser Unterschied jedoch bedeutungslos.

Zweiter Schritt: Überflüssiges

Die einfachsten Maßnahmen zur Verschlankung des Codes ist das Weglassen von Elementen, die in Groovy optional sind, und die Benutzung von masse-reduzierenden Spracheigenschaften:

  • public” ist der Default-Modifier und kann weggelassen werden.
  • return” an letzter Stelle einer Methode benötigt man nicht, wenn die letzte Expression (z.B. eine Variable) zurückgegeben werden soll.
  • ;” am Zeilenende ist überflüssig.
  • Normale Strings werden mit einfachen statt doppelten Hochkommata gebildet. Doppelte Hochkommata haben eine besondere Bedeutung.
  • Member-Variablen ohne die Modifier public, private oder protected werden automatisch als Properties interpretiert. Dies bedeutet, dass Getter- und Setter-Methoden im Byte-Code generiert werden, und dass Zugriffe auf die Property automatisch auf diese Methoden umgelenkt werden.

Durch die Umsetzung dieser Vereinfachungen wird das Ergebnis bereits kürzer und – meiner Meinung nach – besser lesbar:

 1 class HumanBeing {
 2     String name
 3 
 4     public HumanBeing(String name) {
 5         this.name = name
 6     }
 7 
 8     String computeFromName(DoWithName code) {
 9         code.withName(name)
10     }
11 
12     public static void main(String[] args) {
13         HumanBeing son = new HumanBeing('Jannek')
14         son.name = 'Niklas'
15         DoWithName code = new DoWithName() {
16             @Override
17             String withName(String name) {
18                 name + name
19             }
20         }
21         System.out.println(son.computeFromName(code))
22     }
23 
24     interface DoWithName {
25         String withName(String name)
26     }
27 }

Dritter Schritt: Optionale Typen

Die Angabe von Typen für Variablen und Parameter ist in Groovy freiwillig. Aus diesem Grund benötigt Groovy ein weiteres Schlüsselwort def, um Variablen ohne Typ und ohne Modifier deklarieren zu können. Hier die neue Fassung – ohne explizite Typen und ohne System.out, da println als globale Funktion verfügbar ist:

 1 class HumanBeing {
 2     def name
 3 
 4     public HumanBeing(name) {
 5         this.name = name
 6     }
 7 
 8     def computeFromName(code) {
 9         code.withName(name)
10     }
11 
12     public static void main(args) {
13         HumanBeing son = new HumanBeing('Jannek')
14         son.name = 'Niklas'
15         def code = new DoWithName() {
16             @Override
17             def withName(name) {
18                 name + name
19             }
20         }
21         println(son.computeFromName(code))
22     }
23 
24     interface DoWithName {
25         def withName(name)
26     }
27 }

Statische Typisierung

Seit Groovy 2 erfüllt die Sprache auch die Wünsche derer, die aus Sicherheits- oder Performance-Gründen nicht auf strenge Typprüfung verzichten wollen. Es existieren zwei Annotationen, die wahlweise für eine vollständige Typdeklaration oder für einzelne Methoden verwendet werden können:

  • @TypeChecked behält die dynamischen Eigenschaften von Groovy bei, überprüft aber, ob alle Parameter und Variablen auch konform zu ihrer Typdeklaration verwendet werden. Diese Annotation hat keine Auswirkung auf den generierten Byte-Code.
  • @CompileStatic sorgt tatsächlich für Byte-Code, der zusätzlich zur Typprüfung auch zur Generierung von statisch gebundenen Methodenaufrufen führt. Daher kann man mit Hilfe dieser Annotation Groovy-Programme schreiben, welche die gleiche Performance-Charakteristik aufweisen wie äquivalente Java-Programme. Meist bedeutet dies: Sie sind schneller.

Die statische Typprüfung bei Groovy ist um einiges rafinierter als ein Java-Compiler. Viele Typen kann Groovy statisch auflösen, die bei Java einen Cast erfordern. Beispielsweise ist

def son = new HumanBeing(name: 'Jannek')
println son.name

statisch compilierbar, da der Groovy-Compiler weiß, dass die Variable son eine Instanz der Klasse HumanBeing enthält.

Vierter Schritt: Default-Konstruktoren

Ohne dass man etwas tun müsste, erzeugt Groovy für jede Klasse einen Konstruktor, der das initiale Setzen einzelner oder aller Properties mit Hilfe von benannten Parametern. Damit können wir auf den HumanBeing-Konstruktor ganz verzichten, müssen jedoch die Instanziierung ein klein wenig verändern:

HumanBeing son = new HumanBeing(name: 'Jannek')

Auch die in Java verbreitete Form von Konstruktoren mit der Aufzählung aller Properties im Konstruktor, können wir bei Bedarf mit der Annotation @TupleConstructor erzeugen.

Letzte Schritte: Closures

Erst mit JDK 1.8 hat Java Lambda-Ausdrücke spendiert bekommen. Groovy besitzt dieses wichtige Konstrukt schon von Beginn an – unter dem Namen Closures. Closures sind nichts anderes als “Codeblöcke”, die – genau wie andere Objekte auch – zugewiesen, als Berechnungsergebnis zurückgegeben und als Parameter übergeben werden können. Ausführen kann man diese Codeblöcke, indem man sie wie Funktionen aufruft, gegebenenfalls mit Parametern.

In Groovy werden Closures durch geschweifte Klammern gekennzeichnet. Hier ein kurzes Beispiel:

def istTeilbar = {zahl, teiler -> (zahl % teiler) == 0}
assert istTeilbar(35, 7) == true

Gekoppelt mit der Möglichkeit eine Closure als letzten Parameter außerhalb der Klammern zu schreiben, werden wir nicht nur das Interface DoWithName los, sondern können den Aufruf der computeFromName-Method ohne temporäre Variable vornehmen. Hier der Code nach der abermaligen Verkürzung:

 1 class HumanBeing {
 2     def name
 3 
 4     def computeFromName(code) {
 5         code(name)
 6     }
 7 
 8     public static void main(args) {
 9         HumanBeing son = new HumanBeing(name: 'Jannek')
10         son.name = 'Niklas'
11         println(son.computeFromName() { name -> name + name } )
12     }
13 }

Tatsächlich lässt sich die letzte Zeile der main-Methode noch weiter vereinfachen, wenn man auf den Default-Parameter it von Closures zurückgreift und ein paar unnötige Klammern weglässt:

println son.computeFromName { it + it }

Fertiger Code des Kapitels

Closures im GDK

Closures werden dann zu einem mächtigen Sprachmittel, wenn auch die eingesetzten Bibliotheken deren Einsatz fördern. Das Groovy Development Kit (abgekürzt GDK) tut genau das. So existieren für alle Collections zahlreiche Methoden, die mit Closures arbeiten, z.B.:

[1, 2, 3, 4].findAll {it % 2 == 0}.each {println it}

Doch auch an zahllosen anderen Stellen des GDK kommen Closures zum Einsatz, wie z.B. beim sicheren Zugriff auf Ressourcen oder beim Einsatz von Nebenläufigkeitsparadigmen wie Aktoren, Agenten und Dataflows.

Echte Assertions

Auf den ersten Blick könnte man vermuten, dass Groovys assert Schlüsselwort das gleiche tut wie Java – und in erster Näherung ist das auch richtig: Ein assert erhält als Parameter einen booleschen Ausdruck, der zur Laufzeit ausgewertet wird und im false-Falle zum Abbruch des Programms mit einem AssertionError führt. Assert-Ausdrücke in Groovy haben jedoch zwei grundlegende Unterschiede:

  • Sie müssen nicht eingeschaltet und können auch nicht ausgeschaltet werden2. Dadurch wird assert zu einem Partner, auf den man sich verlassen kann.
  • Die von einem fehlschlagenden Assert gelieferten Fehlermeldungen sind sehr detailliert und aussagekräftig, man nennt dieses Feature daher Power Assertions. Ein Beispiel:
    def actual = [1, 2, 3]
    def expected = [1, 2, 4]
    assert actual == expected
    

    ergibt die folgende Fehlermeldung:

    Assertion failed:
    assert actual == expected
           |      |  |
           |      |  [1, 2, 4]
           |      false
           [1, 2, 3]
           at MyProgramm.aMethod(MyProgramm:42)
    

Diese beiden Eigenschaften machen assert zum Assertion-Konstrukt erster Wahl; und das auch in [JUnit]{#anhang-junit}-Testfällen, in denen man in Java ein Assert.assertEquals oder ähnliches einsetzen würde.

Meta-Programmierung und weitere Features

Groovy hat noch zahlreiche weitere nützliche Features; viele davon fallen in das Gebiet der Metaprogrammierung:

  • Analyse und Veränderung von Programmverhalten zur Laufzeit, auch Dynamic Groovy genannt. Beispielsweise kann man jeder Klasse zusätzliche Methoden spendieren:
    String.metaClass.hellofy = { "Hello, $delegate!" }
    assert 'Johannes'.hellofy() == 'Hello, Johannes!'
    
  • AST Transformationen werden vom Compiler ausgewertet und erlauben fast beliebige Veränderung des abstrakten Syntaxbaums eines Programmes vor dessen Ausführung. Zahlreiche nützliche Transformationen sind bereits in Groovy eingebaut, wie z.B. @TailRecursive:
    @TailRecursive
    long sizeOfList(list, counter = 0) {
        if (list.size() == 0)
            return counter
        return sizeOfList(list.tail(), counter + 1)
    }
    // Without @TailRecursive a StackOverFlowError
    // is thrown.
    assert sizeOfList(1..10000) == 10000
    

Hier noch eine kleine, hochgradig unvollständige Featureliste, um dem Leser den Mund wässrig zu machen:

Eine kurzweilige Variante, um sich mit Groovy vertraut zu machen, ist das Projekt GroovyKoans: eine Sammlung von fehlschlagenden Testfällen, die man zum Laufen bringen soll. Viel Spaß!

  1. Groovy macht das immer, es sei denn, eine Datei, eine Klasse oder eine Methode wurde explizit mit @CompileStatic markiert, was zu statischer Kompilierung à la Java führt – und damit auch zu Method-Dispatching anhand des statischen Compiletime-Typs.
  2. In Java ist die Auswertung von asserts per Default ausgeschaltet und muss über die Schalter -ea bzw. -enableassertions explizit aktiviert werden. Die Folge: asserts werden in “normalen” Java-Programmen kaum eingesetzt.

Anhang C: Quellen und Literatur

[Beck 2003] Kent Beck: Test-Driven Development: By Example. Addison-Wesley, 2003.

[Beck 2010] Kent Beck: Test Driven Development. Four video and screencast episodes by Kent Beck. The Pragmatic Bookshelf.

[Evans 2030] Eric J.Evans: Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.

[Freeman+Price 2020] Steve Freeman, Nat Price: Growing Object-Oriented Software, Guided by Tests. Addison-Wesley, 2010.

[Meyer 1997] Betrand Meyer: Object-Oriented Software Construction. Prentice Hall PTR, 2nd edition, 1997.

[Schmeh 2013] Klaus Schmeh: Kryptografie. Verfahren - Protokolle - Infrastrukturen. dpunkt.verlag, 5. Auflage, 2013.

[Singh 2001] Simon Singh: Geheime Botschaften. Die Kunst der Verschlüsselung von der Antike bis in die Zeiten des Internet. Deutscher Taschenbuch Verlag, 2001.