Closures im Querschnitt

Modularisierung von Crosscutting Concerns mit Closures

Paul Häder

Die Diskussion über die Unterstützung von Closures in Java nimmt in letzter Zeit deutlich Fahrt auf. Viele Sprachen unterstützen Closures bereits. Ab JDK 7 werden sie nun möglicherweise auch in Java aufgenommen. Closures sind mächtige Konstrukte mit diversen Anwendungsgebieten. Dieser Artikel zeigt, dass zum Beispiel einige Probleme, die man mit aspektorientierter Programmierung lösen kann, zu einem gewissen Umfang auch mit Closures angegangen werden können.

Angesichts der immer stärker ansteigenden Komplexität heutiger Softwaresysteme kommt Programmeigenschaften wie Lesbarkeit, Wartbarkeit und Evolutionsfähigkeit eine zunehmende Bedeutung zu. Der von Dijkstra geprägte Begriff Separation of Concerns beschreibt das Grundprinzip, mit dessen Hilfe diese Eigenschaften verbessert und die Komplexität beherrscht werden können: die Zerlegung eines Systems in unterschiedliche Einheiten, die sich funktionell möglichst wenig überschneiden. In einigen Fällen ist solch eine klare Modularisierung jedoch nicht einfach möglich. Sehen wir uns beispielsweise Listing 1 an.

Listing 1
----------------------------------------------------------------------
long tBefore = System.nanoTime();
try {
   doSomething();
}
finally {
   long tAfter = System.nanoTime();
   long tElapsed = tAfter - tBefore;
   logTime("something", tElapsed);
}

Dieser Codeabschnitt ist relativ unschön. Die eigentliche Geschäftslogik besteht lediglich aus dem Methodenaufruf doSomething(), ist jedoch mit einer Performance-Logging-Funktionalität vermengt. In einer realen Anwendung könnte es viele Aufrufe einer Menge unterschiedlicher Methoden geben, deren Ausführungszeiten wir loggen möchten, um beispielsweise bestimmte QoS-Eigenschaften des Systems zusichern zu können. Es handelt sich hier um ein typisches Exemplar eines Crosscutting Concerns. So werden Features eines Systems bezeichnet, deren inhärente Eigenschaft es ist, dass sie nur querschneidend zur Klassenstruktur implementiert werden können. In der Tat ist Logging das typische Beispiel für Crosscutting Concerns und um aspektorientierte Programmierung zu motivieren. Aus diesem Grund wurde es bewusst auch hier für die vergleichende Betrachtung von AOP und Closures ausgewählt.

AOP

Java bietet bis heute keine Möglichkeit, Crosscutting Concerns hinreichend zu modularisieren. Hier kommt die Aspektorientierung ins Spiel, denn AOP verspricht eine Modularisierung von Crosscutting Concerns in so genannten Aspekten. Die am weitesten verbreitete aspektorientierte Sprache ist AspectJ. Ein in AspectJ geschriebener Aspekt, der das Logging aus dem obigen Beispielprogramm modularisiert, ist in Listing 2 zu sehen.

Listing 2
------------------------------------------------------------------
aspect PerformanceLoggingAspect {
   pointcut doSomething():
      execution(void MyClass.doSomething());
   around(): doSomething() {
      long tBefore = System.nanoTime();
      proceed();
      long tAfter = System.nanoTime();
      long tElapsed = tAfter - tBefore;
      logTime("something", tElapsed);
   }
}

Zunächst definieren wir einen Pointcut, also eine Menge von Punkten im Programmfluss, die  aus der Basisapplikation „herausgeschnitten“ werden sollen. In diesem Fall wollen wir einfach erreichen, dass alle Ausführungen der Methode MyClass.doSomething() von dem Aspekt erfasst werden. Der eigentliche Logging-Code folgt in Form eines Around Advice, der um die abgefangene Methode doSomething() „herumgelegt“ wird; proceed() ruft diese auf. Die try/finally-Blöcke können wir hier im Übrigen weglassen, da sie vom AspectJ-Compiler generiert werden. Zur Kompilierzeit oder erst zur Klassenladezeit wird schließlich der Aspektcode an den entsprechenden Stellen mit dem Basiscode verwoben. Das Resultat ist eine saubere Trennung von Kernapplikation und querschneidender Funktionalität. Die Zeiten aller Ausführungen der Methode werden nun gemessen, ohne dass redundanter Code notwendig ist. Das Logging ist modularisiert.

Closures

Was haben Closures mit alldem zu tun? Closures, die auf deutsch auch als Funktionsabschlüsse bezeichnet werden, sind laut Wikipedia „ein semantisches Konzept, das eine Funktion zusammen mit einer Umgebung bezeichnet. Wenn aufgerufen, kann die Funktion Elemente der Umgebung referenzieren, die für ihre Berechnung notwendig sind“. Diese „Elemente der Umgebung“ sind zum Beispiel Variablen, die außerhalb der Closure definiert sind. Der strengen Definition nach müssen zudem auch andere Anweisungen in einer Closure dieselbe Bedeutung haben, als seien sie nur Teil des umgebenden Kontextes. Ein return sollte zum Beispiel von der umgebenden Methode zurückkehren, nicht nur von der Closure. Analog muss this (bzw. self, je nach Sprache) in einer Closure auf dasselbe Objekt zeigen, auf das es auch außerhalb der Closure verweist, insbesondere jedoch nicht auf eine Art Funktionsobjekt.

Der Lisp-Dialekt Scheme wurde in den Siebziger Jahren am MIT entwickelt und kann als die erste Sprache angesehen werden, die Closures vollständig unterstützte. Später kam mit Smalltalk die erste objektorientierte Sprache mit Closures. In Smalltalk gibt es so genannte Blöcke: Anweisungsfolgen, die freie Variablen im Kontext der umgebenden Methode binden, also Closures darstellen. Außerdem verhalten sich self und die return-Syntax wie oben beschrieben. Die Closure ist ihrerseits ein Objekt und besitzt eine Methode für ihre Ausführung.

Unter den heute verwendeten Sprachen, die Closures unterstützen, ist zum einen die Gruppe der funktionalen Sprachen wie zum Beispiel Haskell zu nennen. Zum anderen finden Closures auch in dynamischen Programmiersprachen wie Groovy und Ruby Verwendung. In C# sollen sie ab Version 3.0 vollständige Unterstützung finden.

Programmiersprachen mit Closures
Dies ist eine Auswahl von Programmiersprachen, die eine volle Unterstützung von Closures bieten:

  • Common Lisp
  • Groovy
  • Haskell
  • Lua
  • ML
  • Perl
  • Ruby
  • Scala
  • Scheme
  • Smalltalk
Closures in Java

Es gibt derzeit verschiedene Vorschläge dazu, wie Closures in Java aussehen sollten. Ein Vorschlag wird von Gilad Bracha, Neal Gafter, James Gosling und Peter von der Ahé ausgearbeitet. Er beschreibt „echte“ Closures im Sinne der oben gegebenen Definition. Dieser Spezifikation zufolge wäre z.B. die Syntax für eine einfache Closure, die den vollständigen Namen einer gegebenen Person zurückgibt, wie hier zu sehen:

{Person person => 
String firstName = person.getFirstName();
String lastName = person.getLastName();
firstName + " " + lastName
}

Zunächst werden die Parameter der Closure angeführt. In diesem Fall handelt es sich lediglich um ein Objekt vom Typ Person. Weitere Parameter würden kommasepariert folgen. Nach dem Pfeil („=>“) stehen optional eine beliebige Anzahl von Anweisungen und schließlich – ebenfalls optional – ein Ausdruck, der das Resultat der Closure darstellt. Die gesamte Closure wird in geschweifte Klammern gefasst.

Des Weiteren sieht die vorgeschlagene Spezifikation Funktionstypen vor. Diese ermöglichen es dem Programmierer, eine Closure einer Variablen oder einem Methodenparameter zuzuweisen, was die Mächtigkeit der Closures deutlich erhöht. Der folgende Code zeigt die Zuweisung unserer Beispiel-Closure an die Variable fullName.

{Person => String} fullName =
  {Person person => person.getFirstName() + " "  + person.getLastName()};

Auf der linken Seite der Zuweisung stehen die Typen der Parameter vor, der Rückgabetyp der Closure nach dem „=>“.

Der Compiler generiert für alle Funktionsobjekte eine Methode namens invoke(). Diese ruft den Code der Closure auf. Typ und Anzahl der Parameter sowie der Rückgabetyp von invoke() entsprechen denen der Closure. Wir sehen hier eine denkbare Anwendung von fullName:

for (Person person : persons) {
   System.out.println(fullName.invoke(person));
}

Der Code gibt einfach die vollständigen Namen aller Personen in einer Liste aus.

Ein Gegenvorschlag zu der beschriebenen Spezifikation stammt von Joshua Bloch, Doug Lea und Bob Lee. Sie argumentieren, dass Funktionstypen das Typsystem von Java zu sehr verkomplizieren würden. Ihr Vorschlag ist daher weniger tiefgreifend und besteht in der Hauptsache darin, die Syntax für anonyme innere Klassen zu vereinfachen: Closures without Complexity.

Ein anderes Gegenargument – neben der steigenden Komplexität der Sprache – betrifft die Vermischung funktionaler und objektorientierter Paradigmen. „Wer Closures will, soll Haskell benutzen“ ist da bisweilen zu hören. Andere sind der Meinung, Closures kämen zu spät, da sie sich zu einem früheren Zeitpunkt deutlich besser in die Sprache hätten integrieren lassen.

Crosscutting Concerns mit Closures

Kehren wir zu unserem Ausgangsbeispiel zurück. Die Frage ist, wie die Logging-Funktionalität mithilfe von Closures in die Kernapplikation eingefügt werden kann. Wir werden dazu Version 0.5 der Spezifikation von Bracha et al. benutzen und diese auf das eingangs eingeführte Beispiel anwenden. Demzufolge kann man eine statische Methode schreiben, wie sie Listing 3 zeigt.

Listing 3
----------------------------------------------------------------
public static  T time(String id, {=> T throws E} 
 code) throws E {
   long tBefore = System.nanoTime();
   try {
      return code.invoke();
   }
   finally {
      long tAfter = System.nanoTime();
      long tElapsed = tAfter - tBefore;
      logTime(id, tElapsed);
   }
}

Zugegebenermaßen ist die Methodensignatur anfangs etwas schwer zu lesen. Die Methode bekommt zwei Argumente: eine ID des zu messenden Codeabschnitts und eine Closure. Die Closure selbst bekommt keine Argumente und hat den generischen Typ T, der gleichzeitig auch der Rückgabetyp der Methode time() ist. Eventuell geworfene Exceptions werden mit dem ebenfalls generischen E typisiert. Der Methodenrumpf enthält die Implementierung der Performance-Logging-Funktionalität, wobei mit code.invoke() der Codeblock ausgeführt wird, welcher der Methode als Closure übergeben wurde.

Vorausgesetzt, dass eine Klasse einen statischen Import der time() Methode besitzt, kann diese wie folgt benutzt werden:

time("something", {=>
    doSomething();
});

Die so genannte Control Invocation Syntax erlaubt eine noch sauberere Schreibweise:

time("something") {
    doSomething();
}

Die Methode time kann somit ähnlich wie ein Java-Schlüsselwort benutzt werden. Closures ermöglichen es uns auf diese Weise, das Logging deutlich besser modularisiert in das Programm einzubinden als es noch eingangs in Listing 1 der Fall war.

Vergleich

Sehen wir uns noch einmal an, wie aspektorientierte Programmierung auf der einen und Closures auf der anderen Seite das Modularisierungsproblem in unserem konkreten Beispiel lösen. Beiden Ansätzen gelingt es, die Geschäftslogik von der Logging-Funktionalität zu separieren. Sie zentralisieren letztere im Advice eines Aspekts beziehungsweise in einer statischen Methode einer gewöhnlichen Klasse. Dabei gibt es mit proceed() bzw. invoke() jeweils die Möglichkeit, den zu messenden Codeabschnitt aufzurufen.

Ein signifikanter Unterschied zwischen den beiden Herangehensweisen betrifft die Information, wo das Logging anzuwenden ist. In AspectJ wird dies in der Pointcut-Deklaration ausgedrückt. Pointcuts sind sehr flexibel, da sie viele verschiedene Arten von Ereignissen im Programmfluss abfangen können, wie z.B. Methodenaufrufe, Feldzugriffe etc. Zudem können Wildcards verwendet werden. Während beim aspektorientierten Ansatz die Kernapplikation nichts von der querschneidenden Funktionalität „wissen“ muss, kann dies durch Verwendung von Closures nicht erreicht werden. Hier wird der Methodenaufruf um all jene Codeabschnitte „gelegt“, die dem Performance-Logging unterzogen werden sollen. Die statische Methode fügt sich dabei in den Code ein als sei sie Bestandteil der Programmiersprache. Dies ist insofern ebenfalls sehr flexibel, als dass das Logging auf jeden beliebigen Code-Block angewendet werden kann. Es liegt in der Natur der beiden Ansätze, dass aspektorientierte Programmierung vor allem geeignet ist, Punkte im Programmfluss auszuwählen, während es die gezeigte Verwendung von Closures besonders einfach macht, mit Codeblöcken zu arbeiten. Darüber hinaus erleichtert es AOP, das Logging nach Wunsch auszuschalten. Hierfür muss lediglich der Aspekt nicht deployt werden. Dies ist mit Closures auf keine vergleichbar elegante Weise möglich.

Fazit

Wir haben gesehen, dass Closures in der von Bracha et al. vorgeschlagenen Form in der Lage sind, Crosscutting Concerns besser zu modularisieren als Java es derzeit ermöglicht. Ein wichtiges Element der entsprechenden Spezifikation ist hierbei die Control Invocation Syntax. Diese erlaubt es, eine Closure als Argument an eine Methode zu übergeben als sei diese Bestandteil der Programmiersprache. Ein Vorteil dieser Syntax wäre daher auch die Tatsache, dass zukünftige Erweiterungen von Java leichter als Erweiterungen der API anstatt als Spracherweiterungen vorgenommen werden könnten. Closures würden auf diese Weise helfen, eine Überfrachtung der Sprache mit zu vielen Features zu vermeiden.

Im Vergleich zu Closures gelingt mit AOP eine noch sauberere Modularisierung von Crosscutting Concerns. Im Gegensatz zu Closures wurde die aspektorientierte Programmierung ja ins Leben gerufen, um diese Art von Problemen zu lösen. Und anders als für AOP erscheint ein formelles Spezifikationsbestreben für Closures in Form eines JSR in nächster Zeit als wahrscheinlich.

Paul Häder ist Diplom-Informatiker und hat an der TU Berlin studiert. Er beschäftigt sich seit einigen Jahren mit Java-Technologien sowie aspektorientierter Programmierung und arbeitet bei der Accenture Technology Solutions GmbH.
Geschrieben von
Paul Häder
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.