Als Erstes schauen wir uns Traits noch einmal etwas genauer an. Wie Java-Interfaces können sie abstrakte Methoden definieren. Aber sie können noch viel mehr. Sie können auch konkrete Methoden enthalten. Sie können auch Felder (vals oder vars) enthalten. Sie können eigentlich alles, was Klassen auch können, mit einer signifikanten Ausnahme: Sie können keine Parameter haben und sie können nicht instanziert werden.
Als einfaches Beispiel wollen wir das Tierreich bemühen: Animal als Basisklasse, davon abgeleitet Fish bzw. Bird, die jeweils typische Dinge tun können, und zwar swim bzw. fly:
class Animal(val name: String)class Fish(name: String) extends Animal(name) {def swim = "I can swim."}class Bird(name: String) extends Animal(name) {def fly = "I can fly."}
Soweit, so gut. Doch wie gehen wir mit Enten um? Diese sind natürlich Vögel, können aber auch schwimmen. In Java, wo wir nur eine eindimensionale Vererbungshierarchie haben, müssten wir wohl oder übel swim für Duck neu implementieren. Ganz anders in Scala.
Zunächst definieren wir den Trait CanSwim und kopieren swim vom Fish, d.h. wir schaffen einen Trait mit einer konkreten Methode:
trait CanSwim {def swim = "I can swim."}
Dann mixen wir diesen Trait in den Fish und löschen dort swim:
class Fish(name: String) extends Animal(name) with CanSwim
Zu guter Letzt schreiben wir unsere Duck folgendermaßen:
class Duck(name: String) extends Bird(name) with CanSwim
Zum Beweis, dass Duck sowohl fliegen als auch schwimmen kann, gehen wir in die REPL:
scala> import com.weiglewilczek.demoscala.traits._import com.weiglewilczek.demoscala.traits._scala> val donald = new Duck("Donald")donald: ...scala> donald.flyres0: java.lang.String = I can fly.scala> donald.swimres1: java.lang.String = I can swim.
Wir sehen, dass Donald nicht nur fliegen, sondern auch schwimmen kann. Mit Traits können wir also denselben Effekt wie bei klassischer Mehrfachvererbung erzielen, ohne uns dabei auf typische Problemsituationen wie das Diamond Problem einzulassen. Ohne allzu tief einzusteigen, lautet die Lösung in Scala "Linearisierung": Der Compiler bringt alle vererbten und hineingemixten Typen in eine lineare Reihenfolge, bei der das Objekt selbst "ganz unten" steht. Dadurch ist immer klar, wer aufgerufen wird, was mit super gemeint ist und dass das Objekt das Verhalten der Supertypen modifiziert und nicht umgekehrt. Für Details sei auf die einschlägige Literatur verwiesen (Odersky, Spoon, Venners: „Programming in Scala“, Kapitel 12.6).
Nun wollen wir noch zeigen, wie wir AOP-artig Methodenaufrufe abfangen können. Dazu definieren wir uns zunächst einen weiteren Trait MichaelBuble, ebenfalls ein Vogel, aber einer, der ganz besonders fliegt und daher fly überschreibt:
trait MichaelBuble extends Bird {override def fly = "I feel good!"}
Im Unterschied zu vorher mixen wir diesen Trait nicht direkt in eine unsere Klassen hinein, sondern tun dies erst beim Erzeugen von Objekten. Auf diese Art und Weise können wir auch Klassen modifizieren, die von "anderen" stammen, also insbesondere 3rd-Party-Libraries. In der REPL könnte das folgendermaßen aussehen:
scala> val michael = new Bird("Michael") with MichaelBublemichael: ...scala> michael.flyres0: java.lang.String = I feel good!
Wie wir sehen, fliegt unser Vogel, der ja immer noch ein Bird ist, nun ganz anders. Wir haben im Prinzip die Methode fly abgefangen und etwas anderes gemacht, ohne das eigentliche Verhalten einzubinden, d.h. ohne die eigentliche fly-Methode aufzurufen. Das können wir aber auch tun:
trait MichaelBuble extends Bird {override def fly = "I feel good and " + super.fly}
In dieser Variante bringt der Trait MichaelBuble zunächst neues Verhalten ins Spiel, um anschließend den Supertyp, hier also Bird, einzubinden. Das Ergebnis sieht natürlich wie folgt aus:
scala> val michael = new Bird("Michael") with MichaelBublemichael: ...scala> michael.flyres0: java.lang.String = I feel good and I can fly.
Ähnlich könnten wir zuerst den Supertyp aufrufen, um anschließend das Ergebnis weiter zu bearbeiten.
Zusammenfassend können wir mit Traits dasselbe erreichen, wie in der aspektorientierten Programmierung mit Method Interception: Wir können vor dem eigentlichen Aufruf etwas tun, dann optional den eigentlichen Aufruf durchführen und danach noch etwas tun. Wenn wir dieses einfache Beispiel auf "ernsthafte" Szenarien übertragen, z. B. Logging oder Security, dann wird klar, welch mächtiges und zugleich einfach anzuwendendes Werkzeugs Traits darstellen.
Der komplette Code dieses Beispiels ist auf github unter http://wiki.github.com/weiglewilczek/demo-scala verfügbar. Fragen oder Kommentare sind jederzeit erwünscht.



