Sonntag, 12. Februar 2012


Artikel

Juli 2010 | Artikel

Mehrfachvererbung und AOP "light" mit Scala

(Link zum Artikel: http://www.entwickler-magazin.de/jaxenter/artikel/3241)

Die Advanced-Scala-Serie

Text: Heiko Seeberger
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Im letzten Artikel haben wir gezeigt, wir wir allein mit Scala-Sprachmitteln Dependency Injection umsetzen können. Dabei spielen Traits eine wichtige Rolle. Heute wollen wir demonstrieren, dass wir mit Traits noch viel mehr anfangen können, und zwar Mehrfachvererbung und aspektorientierte Programmierung "light". Mit "light" ist gemeint, dass wir natürlich nicht die Mächtigkeit echter AOP-Sprachen wie AspectJ komplett nachbilden können. Aber das, was uns dynamische Proxies, Spring AOP oder neuerdings auch JEE bieten, nämlich Abfangen von Methodenaufrufen aka Method Interception, können wir in Scala wieder ganz einfach und nur mit Sprachmitteln schaffen.

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:

  1. class Animal(val name: String)
  2. class Fish(name: String) extends Animal(name) {
  3. def swim = "I can swim."
  4. }
  5. class Bird(name: String) extends Animal(name) {
  6. def fly = "I can fly."
  7. }

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:

  1. trait CanSwim {
  2. def swim = "I can swim."
  3. }

Dann mixen wir diesen Trait in den Fish und löschen dort swim:

  1. class Fish(name: String) extends Animal(name) with CanSwim

Zu guter Letzt schreiben wir unsere Duck folgendermaßen:

  1. class Duck(name: String) extends Bird(name) with CanSwim

Zum Beweis, dass Duck sowohl fliegen als auch schwimmen kann, gehen wir in die REPL:

  1. scala> import com.weiglewilczek.demoscala.traits._
  2. import com.weiglewilczek.demoscala.traits._
  3. scala> val donald = new Duck("Donald")
  4. donald: ...
  5. scala> donald.fly
  6. res0: java.lang.String = I can fly.
  7. scala> donald.swim
  8. res1: 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:

  1. trait MichaelBuble extends Bird {
  2. override def fly = "I feel good!"
  3. }

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:

  1. scala> val michael = new Bird("Michael") with MichaelBuble
  2. michael: ...
  3. scala> michael.fly
  4. res0: 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:

  1. trait MichaelBuble extends Bird {
  2. override def fly = "I feel good and " + super.fly
  3. }

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:

  1. scala> val michael = new Bird("Michael") with MichaelBuble
  2. michael: ...
  3. scala> michael.fly
  4. res0: 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.

Heiko Seeberger ist geschäftsführender Gesellschafter der Weigle Wilczek GmbH und verantwortlich für die technologische Strategie des Unternehmens mit den Schwerpunkten Java, Scala, OSGi, Eclipse RCP, Lift und Akka. Zudem ist er aktiver Open Source Committer, Autor zahlreicher Fachartikel und Redner auf einschlägigen Konferenzen.

andere Artikel dieser Serie

Kommentare

Gravatar Trepper 27.07.2010
um 12:37 Uhr
Ein tolles Beispiel für die Mächtigkeit von Scala! Je länger ich darüber nachdenke, desto mehr komme ich zu dem Schluss, dass Java für viele praktische Aufgaben einfach nicht Mächtig genug ist und deshalb für alles mögliche zusätzliche Frameworks benötigt werden oder die Sprache mit Annotationen, Reflection und Expression Languages aufgeweicht wird. Im Vergleich mit Scala wird deutlich, dass Java ein Dinosaurier ist. #zitieren
Gravatar Mike 14.10.2010
um 21:07 Uhr
Wirklich ein tolles Feature erst bei Erstellung eines Objekt ein Trait "einzumixen". Nur müssen wir ja nach diesem Ansatz im Trait den genauen Methodennamen kennen und überschreiben. Für richtige "cross-cutting"-Themen wie Logging oder Security scheint mir dieser Ansatz zu unflexibel. Oder gibt es noch mir unbekannte sprachliche Mittel?

Danke und Gruss
#zitieren
Gravatar Hemeroc 27.10.2010
um 11:34 Uhr
Das was Mike anmerkt ist auch das Einzige was mich stört. Gibt es da einen passenden Ansatz in scala oder muss man hier wieder zu AspectJ zurück?
Ansonsten danke für den Artikel!

LG
Hemeroc
#zitieren
Gravatar Trepper 17.12.2010
um 08:48 Uhr
@Redktion: Merkt die Redaktion eigentlich nicht, dass Beiträge wie die von "au pair" Spam sind, oder ist es ihr egal? #zitieren
Gravatar Redaktion JAXenter 17.12.2010
um 09:06 Uhr
Die Redaktion bemerkt Spam-Kommentare und entfernt diese regelmäßig. Wenn trotzdem kurzzeitig Spammer durchkommen, dann sind wir für jeden Hinweis dankbar! #zitieren