Unterstützung von Traits ab Groovy 2.3

Vor kurzem ist das Release 2.3 von Groovy herausgekommen. Aus den Release Notes kann man die Neuerungen herauslesen:

  • Offizielle Unterstützung des JDK 8
  • neue und aktualisierte AST-Transformationen (@Builder, @Sortable, @Delegate, @Basescript)
  • Performance-Verbesserungen (z. B. Parsen von JSON)
  • Closure-Parameter Typ-Inferenz
  • neue Template-Engine (Markup Template Engine)
  • Klasse groovy.test.GroovyAssert für JUnit 4 (shouldFail, assertScript, …)
  • konfigurierbare Bedingungsblöcke im ConfigSlurper
  • Toolverbesserungen (groovysh und groovyconsole)


Zu beachten ist, dass Groovy 2.3 nun mindestens das JDK 6 benötigt. Die größte Neuerung dieses Releases ist die Einführung von Traits, um Mehrfachvererbung zu ermöglichen. Bisher konnte man so etwas bereits über AST-Transformationen (@Mixin) implementieren, nun sind sie aber Teil des Sprachumfangs und auch nochmal etwas mächtiger geworden.

Traits sehen ähnlich aus wie Interfaces mit abstrakten Methoden, sie können aber wie ab Java 8 Default Implementierungen und zusätzlich auch einen Zustand (gibt es in Java 8 nicht) enthalten.

trait Fahrbar {
    int geschwindigkeit
    void fahren() {
      println "Fahren mit " + "${geschwindigkeit} km/h!"}
}

class Bobbycar implements Fahrbar {}
new Bobbycar(geschwindigkeit:100).fahren()

Abstrakte Methoden verwendet man, um zum Beispiel das Template Method Pattern umzusetzen. Das Trait legt dann einen Algorithmus fest und einzelne Teilschritte können in Subklassen durch Implementierung der abstrakten Methode konkret ausformuliert werden.

trait Greetable { 
  abstract String name() 
  String greeting() { 
    "Hello, ${name()}!" 
  } 
}

class Person implements Greetable { 
  String name() { 'Duke' } 
} 

def p = new Person() 
assert p.greeting() == 'Hello, Duke!'

Traits können übrigens auch von Interfaces ableiten und darüber abstrakte Methoden erben.

interface Named {                                       
  String name()
}
trait Greetable implements Named {                      
  String greeting() { "Hello, ${name()}!" }
}
class Person implements Greetable {                     
  String name() { 'Bob' }                             
}

def p = new Person()
assert p.greeting() == 'Hello, Bob!'                    
assert p instanceof Named                               
assert p instanceof Greetable

diamond-problemJetzt gab es doch eigentlich einen guten Grund, warum man sich damals in den 90ziger Jahren gegen Mehrfachvererbung in Java entschieden hatte. Richtig, es kann zu Konflikten kommen, wenn man von mehreren Klassen mit gleichen Methodensignaturen erbt. Das wird auch als Diamond-Problem bezeichnet, in Anspielung auf das Aussehen der Vererbungshierarchie in Diamenten-Form. Groovy löst dieses Problem nach dem “Last wins”-Prinzip. In dem folgenden Beispiel leitet die Klasse D von B und als letztes von C ab, dementsprechend wird die Implementierung von exec() von C geerbt.

trait A {
  String exec() { 'A' }
}

trait B extends A {
  String exec() { 'B' }
}

trait C extends A {
  String exec() { 'C' }               
}

class D implements B, C {}

def d = new D() 
assert d.exec() == 'C'

Man kann neben der “Last wins”-Strategie auch manuell auflösen, von welchem Trait die Implementierung verwendet werden soll. Dazu muß man aber in der Subklasse exec() überschreiben und kann dort auf die Implementierung des zu verwendenden Traits delegieren.

class D implements B, C {
  String exec() { B.super.exec() }    
}
def d = new D()
assert d.exec() == 'B' 

Groovy ist eine dynamische Sprache und so kann man Traits auch zur Laufzeit implementieren. Bei der Instanziierung einer Klasse kann man mit dem Typ-Konvertier-Operator “as” ein Trait angegeben, von dem dann die neue Instanz zur Laufzeit ableitet. Bei der Erzeugung der Klasse muß man also noch nicht zur Compile-Zeit wissen, von welchen Traits man mal später erben möchte.

trait Extra {
  String extra() { "I'm an extra method" }            
}
class Something {                                       
  String doSomething() { 'Something' }                
}

def s = new Something() as Extra                        
s.extra()                                               
s.doSomething()

Traits mit nur einer abstrakten Methode und beliebigen Default-Implementierungen sind sogenannte SAM-Typen (Single Abstract Method). Genau wie bei Java 8, wo man Lambda-Ausdrücke in diese als Functional Interfaces bezeichneten SAM-Typen einfach per Type Coercion zwingen kann, erlaubt Groovy das Behandeln von Closures als diese speziellen Traits. Überall wo man eine Instanz eines SAM-Traits erwartet, darf auch eine Closure übergeben werden, deren Inhalt dann zur Implementierung dieser einzelnen abstrakten Methode wird.

trait Greeter {
  String greet() { "Hello $name" }        
  abstract String getName()               
}

Greeter greeter = { 'Alice' } 
println greeter.greet()

// oder

void greet(Greeter g) { println g.greet() } 
println greet { 'Alice' }

Traits bringen die Mächtigkeit der Mehrfachvererbung in die Sprache Groovy. Die AST-Transformationen (@Mixin), mit denen man ein ähnliches Verhalten schon in früheren Groovy-Versionen erreichen konnte, sollten nicht mehr verwendet werden. Groovy schließt mit diesem neuen Sprachkonstrukt zu Sprachen wie Scala auf und bietet sogar mehr als die in Java 8 eingeführten Default-Methoden in Interfaces.

Als nächstes Groovy Release ist für Ende 2014 die Version 3.0 geplant. Laut Roadmap sollen dafür das Meta Object Protocol und die Groovy Syntax überarbeitet werden, um bessere Unterstützung für Invoke Dynamic und die neuen Sprachmerkmale von Java 8 zu erreichen. Auch wird es bald eine neue Projektseite geben, der Wegzug von Codehaus ist damit eingeläutet. In diesem Rahmen wird auch die Dokumentation überarbeitet, wobei AsciiDoc zum Einsatz kommt.

Short URL for this post: https://wp.me/p4nxik-25v
This entry was posted in Groovy and Grails and tagged , , , . Bookmark the permalink.

Leave a Reply