Innovation

Aspektorientierte Programmierung, Abfangfunktion und Unity 2.0

Dino Esposito

image: Dino EspositoZweifellos ist die Objektorientierung das Standard-Programmierungsparadigma, dessen Stärken in der Aufteilung eines Systems in dessen Komponenten und in der Beschreibung von Prozessen durch Komponenten liegt. Das objektorientierte (OO) Paradigma ist außerdem hervorragend für die Betrachtung der geschäftsspezifischen Probleme einer Komponente geeignet. Das OO-Paradigma ist jedoch nicht gleichermaßen effektiv, wenn es um übergreifende Probleme geht. Allgemein ausgedrückt, ist ein übergreifendes Problem ein Problem, das mehrere Komponenten eines Systems betrifft.

Um die Wiederverwendbarkeit von komplexem Geschäftslogikcode zu optimieren, wird in der Regel eine Hierarchie von Klassen um den Kern und die primären Geschäftsfunktionen des Systems herum entworfen. Was aber, wenn es um andere, nicht geschäftsspezifische Probleme geht, die die Klassenhierarchie übergreift? Wo ordnen Sie Funktionen wie Zwischenspeichern, Sicherheit und Protokollierung ein? Am wahrscheinlichsten werden sie in jedem betroffenen Objekt wiederholt.

Ein übergreifendes Problem ist nicht die spezifische Verantwortung einer bestimmten Komponente oder Komponentenfamilie und stellt einen Aspekt des Systems dar, der auf einer anderen logischen Ebene behandelt werden muss, einer Ebene jenseits von Anwendungsklassen. Aus diesem Grund wurde vor Jahren ein anderes Programmierparadigma definiert: die aspektorientierte Programmierung (AOP). Das AOP-Konzept wurde in den 90er-Jahren in den Xerox PARC-Laboren entwickelt. Das Team entwickelte außerdem die erste (und immer noch am weitesten verbreitete) AOP-Sprache: AspectJ.

Auch wenn beinahe jeder die Vorteile der AOP anerkennt, ist sie immer noch nicht weit verbreitet. Meiner Meinung nach besteht der Hauptgrund für die begrenzte Einführung im Fehlen geeigneter Tools. Ich bin mir ziemlich sicher, dass es einen bedeutenden Unterschied für AOP bedeuten würde, wenn AOP (auch nur teilweise) durch Microsoft .NET Framework unterstützt würde. Zurzeit können Sie AOP in .NET nur mittels Ad-Hoc-Frameworks anwenden.

Das leistungsfähigste AOP-Tool in .NET ist PostSharp, das Sie auf sharpcrafters.com finden können. PostSharp stellt ein vollständiges AOP-Framework bereit, in dem Sie alle wesentlichen Funktionen für den AOP-Ansatz vorfinden. Sie sollten jedoch wissen, dass zahlreiche Dependency Injection (DI)-Frameworks AOP-Funktionen enthalten.

Beispielsweise finden Sie AOP-Funktionen in Spring.NET, Castle Windsor und – natürlich – Microsoft Unity. Im Fall vergleichsweise einfacher Szenarien, wie Ablaufverfolgung, Zwischenspeichern und die Anordnung der Komponenten in der Anwendungsschicht, reichen in der Regel die Funktionen von DI-Frameworks aus. DI-Frameworks genügen jedoch nicht mehr, wenn es um Domänen- und Benutzeroberflächenobjekte geht. Ein übergreifendes Problem kann sicherlich als eine externe Abhängigkeit betrachtet werden, und DI-Techniken ermöglichen Ihnen das Einfügen externer Abhängigkeiten in eine Klasse.

Der Punkt ist jedoch, dass die DI wahrscheinlich einen Ad-Hoc-Vorabentwurf oder eine Umgestaltung erfordert. Mit anderen Worten, wenn Sie bereits ein DI-Framework verwenden, können Sie leicht einige AOP-Funktionen einführen. Wenn Sie hingegen keine DI verwenden, kann die Einführung eines DI-Frameworks einigen Aufwand erfordern. Dies ist in einem großen Projekt oder während der Aktualisierung eines vorhandenen Systems nicht immer möglich. Bei einem klassischen AOP-Ansatz fassen Sie alle übergreifenden Probleme jedoch in einer neuen Komponenten namens Aspekt zusammen. In diesem Artikel stelle ich Ihnen eine kurze Übersicht über das aspektorientierte Paradigma bereit und zeige Ihnen anschließend die AOP-Funktionen in Unity 2.0.

Eine kurze Einführung in AOP

Ein objektorientiertes Programmierobjekt (OOP-Projekt) besteht aus einer Reihe von Quelldateien, die jeweils eine oder mehrere Klassen implementieren. Das Projekt enthält außerdem Klassen, die übergreifende Probleme behandeln, wie die Protokollierung oder die Zwischenspeicherung. Alle Klassen werden durch einen Compiler verarbeitet und produzieren ausführbaren Code. In AOP ist ein Aspekt eine wiederverwendbare Komponente, die ein Verhalten enthält, das für mehrere Klassen innerhalb des Projekts erforderlich ist. Die tatsächliche Verarbeitung der Aspekte ist von der AOP-Technologie abhängig, die Sie in Betracht ziehen. Im Allgemeinen kann man sagen, dass Aspekte nicht einfach direkt vom Compiler verarbeitet werden. Es ist ein zusätzliches, technologiespezifisches Tool erforderlich, um den ausführbaren Code zu modifizieren, sodass dieser Aspekte berücksichtigt. Wenden wir uns kurz AspectJ zu, einem Java-basierten AOP-Compiler, der das erste jemals entwickelte AOP-Tool darstellt.

In AspectJ verwenden Sie die Java-Programmiersprache, um Klassen zu erstellen, und die AspectJ-Sprache, um Aspekte zu erstellen. AspectJ unterstützt eine angepasste Syntax, mithilfe derer Sie das erwartete Verhalten des Aspekts berücksichtigen. Zum Beispiel kann ein Protokollierungsaspekt angeben, dass vor und nach dem Aufruf einer bestimmten Methode eine Protokollierung durchgeführt wird. Aspekte werden auf bestimmte Weise mit dem regulären Quellcode zusammengeführt und produzieren eine Zwischenversion des Quellcodes, der anschließend in ein ausführbares Format konvertiert wird. In der AspectJ-Terminologie wird die Komponente, die die Aspekte vorab verarbeitet und mit dem Quellcode zusammenführt, als Weaver bezeichnet. Diese produziert ein Ergebnis, das vom Compiler in eine ausführbare Datei gerendert werden kann.

Zusammenfassend lässt sich sagen, dass ein Aspekt einen wiederverwendbaren Teil des Codes beschreibt, den Sie in die vorhandenen Klassen einfügen können, ohne den Quellcode dieser Klassen zu verändern. In anderen AOP-Frameworks (wie dem .NET PostSharp-Framework) ist kein Weaver-Tool enthalten. Der Inhalt eines Aspekts wird jedoch stets vom Framework verarbeitet und führt zu einer Form der Codeinjizierung.

Beachten Sie, dass sich die Einfügung (Injizierung) von Code von der Injizierung einer Abhängigkeit unterscheidet. Die Codeinjizierung bezieht sich auf die Fähigkeit eines AOP-Frameworks, an bestimmten Punkten innerhalb der Klassen, die mit einem bestimmten Aspekt angeordnet sind, Aufrufe für öffentliche Endpunkte in den Aspekt einzufügen. Das PostSharp-Framework ermöglicht Ihnen beispielsweise das Erstellen von Aspekten als .NET-Attribute, die Sie anschließend in den Klassen an Methoden anfügen. PostSharp-Attribute werden in einem späteren Schritt durch den PostSharp-Compiler verarbeitet (dieser könnte auch als Weaver bezeichnet werden). Das Ergebnis ist, dass Ihr Code einen Teil des Codes in den Attributen enthält. Die Injizierungspunkte werden jedoch automatisch aufgelöst. Ihre Aufgabe als Entwickler besteht darin, eine eigenständige Aspektkomponente zu erstellen und diese der Methode einer öffentlichen Klasse anzufügen. Dies einfach, und sogar einfacher als die Codewartung.

Zum Schluss dieser kurzen Übersicht über AOP möchte ich Sie mit einigen Begriffen bekannt machen und deren beabsichtigte Bedeutung erklären. Join Point (Verbindungspunkt) bezeichnet den Punkt im Quellcode der Zielklasse, an dem Sie den Aspektcode injizieren möchten. Pointcut ist eine Sammlung von Verbindungspunkten. Advice bezeichnet den Code, der in die Zielklasse injiziert werden soll. Der Code kann vor, nach und um den Verbindungspunkt herum injiziert werden. Ein Advice ist mit einem Pointcut verknüpft. Diese Begriffe stammen aus der ursprünglichen Definition von AOP und finden sich möglicherweise nicht wörtlich in dem von Ihnen verwendeten AOP-Framework. Ich empfehle Ihnen, dass Sie die Konzepte hinter den Begriffen zu verstehen versuchen, den Säulen von AOP, und dann dieses Wissen anwenden, um die Details eines bestimmten Frameworks besser zu verstehen.

Eine kurze Einführung in Unity 2.0

Unity ist ein Anwendungsblock, der sowohl als Teil des Microsoft Enterprise Library-Projekts als auch als eigenständiger Download verfügbar ist. Die Microsoft Enterprise Library ist eine Sammlung von Anwendungsblöcken, die eine Reihe von übergreifenden Problemen behandeln, die die .NET-Anwendungsentwicklung charakterisieren: Protokollierung, Zwischenspeicherung, Kryptographie, Ausnahmebehandlung und mehr. Die neueste Version der Enterprise Library ist die Version 5.0, die im April 2010 veröffentlicht wurde und Visual Studio 2010 vollständig unterstützt (weitere Informationen finden Sie im Developer Center unter msdn.microsoft.com/library/ff632023).

Unity ist einer der Anwendungsblöcke der Enterprise Library. Unity ist auch für Silverlight verfügbar und im Wesentlichen ein DI-Container mit zusätzlicher Unterstützung für einen Abfangmechanismus, durch den Sie Ihre Klassen etwas aspektorientierter gestalten können.

Abfangfunktion in Unity 2.0

Die Kernidee der Abfangfunktion in Unity besteht darin, Entwicklern die Anpassung der Aufrufkette zu ermöglichen, die für den Aufruf einer Methode auf einem Objekt erforderlich ist. Mit anderen Worten, der Unity-Abfangmechanismus erfasst Aufrufe, die für konfigurierte Objekte erfolgen, und passt das Verhalten der Zielobjekte an, indem zusätzlicher Code vor, nach oder um die reguläre Ausführung der Methoden hinzugefügt wird. Die Abfangfunktion stellt im Wesentlichen ein äußerst flexibles Verfahren dar, um einem Objekt zur Laufzeit in neues Verhalten hinzuzufügen, ohne dessen Quellcode zu verändern und ohne das Verhalten der Klassen im gleichen Vererbungspfad zu beeinflussen. Die Unity-Abfangfunktion stellt eine Möglichkeit der Implementierung des Decorator-Musters dar. Dies ist ein verbreitetes Entwurfsmuster, das die Funktionalität eines Objekts zur Laufzeit erweitern soll, wenn das Objekt verwendet wird. Ein Decorator ist ein Containerobjekt, das eine Instanz des Zielobjekts erhält (und einen Verweis auf dieses behält) und dessen externe Funktionen augmentiert.

Der Abfangfunktionsmechanismus in Unity 2.0 unterstützt sowohl den Abfang von Instanzen als auch von Typen. Außerdem funktioniert die Abfangfunktion unabhängig von der Art, in der das Objekt instantiiert wurde, sei es, dass es durch den Unity-Container erstellt wurde, sei es, dass es sich um eine bekannte Instanz handelt. Im letzten Fall können Sie einfach eine andere, vollständig eigenständige API verwenden. Sie verlieren dann jedoch die Konfigurationsdateiunterstützung. Abbildung 1 zeigt die Architektur der Abfangfunktion in Unity und deren Funktionsweise für eine Objektinstanz, die nicht durch den Container aufgelöst wurde. (Die Abbildung stellt eine leicht überarbeitete Version einer Abbildung dar, die Sie in der MSDN-Dokumentation finden.)

image: Object Interception at Work in Unity 2.0

Abbildung 1 Funktion der Objektabfangfunktion in Unity 2.0

Das Abfangfunktions-Subsystem besteht aus drei Hauptelementen: dem Interceptor (oder Proxy); der Verhaltenspipeline; und dem Verhalten oder Aspekt. An den beiden äußeren Enden der Subsysteme finden Sie die Clientanwendung und das Zielobjekt, d. h. das Objekt, dem zusätzliche Verhaltensweisen zugewiesen werden, die nicht in dessen Quellcode enthalten sind. Nachdem die Clientanwendung für die Verwendung der Abfangfunktions-API von Unity für eine bestimmte Instanz konfiguriert wurde, wird jeder Methodenaufruf über ein Proxyobjekt – den Interceptor – geleitet. Dieses Proxyobjekt betrachtet zunächst die Liste der registrierten Verhaltensweisen und ruft diese über die interne Pipeline auf. Jedes konfigurierte Verhalten kann vor oder nach dem regulären Aufruf der Objektmethode ausgeführt werden. Der Proxy injiziert Eingabedaten in die Pipeline und erhält alle Wiedergabewerte in der Form, wie sie ursprünglich durch das Zielobjekt generiert und wie sie anschließend durch die Verhaltensweisen verändert wurden.

Konfigurieren der Abfangfunktion

Die empfohlene Verwendung der Abfangfunktion in Unity 2.0 unterscheidet sich von früheren Versionen, auch wenn der Ansatz der früheren Versionen zum Zweck der Rückwärtskompatibilität vollständig unterstützt wird. In Unity 2.0 ist die Abfangfunktion nur eine neue Erweiterung, die Sie dem Container hinzufügen, um zu beschreiben, wie ein Objekt tatsächlich aufgelöst wird. Im Folgenden finden Sie den Code, den Sie benötigen, wenn Sie die Abfangfunktion mittels Flusscode konfigurieren möchten:

var container = new UnityContainer();
container.AddNewExtension<Interception>();

Der Container muss Informationen zu den Typen finden, die abgefangen werden sollen, und zu den Verhaltensweisen, die hinzugefügt werden sollen. Diese Informationen können entweder mittels Flusscode oder mittels Konfiguration hinzugefügt werden. Ich finde die Konfiguration besonders flexibel, da Sie Modifizierungen durchführen können, ohne die Anwendung zu verändern und ohne einen neuen Kompilierschritt durchführen zu müssen. Betrachten wir daher den konfigurationsbasierten Ansatz.

Zu Beginn fügen Sie den folgenden Code in die Konfigurationsdatei ein:

<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.
  Configuration.InterceptionConfigurationExtension, 
  Microsoft.Practices.Unity.Interception.Configuration"/>

Der Zweck dieses Skripts besteht in der Erweiterung des Konfigurationsschemas durch neue Elemente und Aliase, die für das Abfangfunktions-Subsystem spezifisch sind. Eine andere notwendige Ergänzung ist die folgende:

<container> 
  <extension type="Interception" /> 
  <register type="IBankAccount" mapTo="BankAccount"> 
    <interceptor type="InterfaceInterceptor" /> 
    <interceptionBehavior type="TraceBehavior" /> 
  </register> 
</container>

Um das gleiche Ergebnis mittels Flusscode zu erzielen, müssen Sie auf dem Containerobjekt AddNewExtension<T> und RegisterType<T> aufrufen.

Betrachten wir nun das Konfigurationsskript etwas genauer. Das Element <extension> fügt dem Container die Abfangfunktion hinzu. Beachten Sie, dass die in diesem Skript verwendete Abfangfunktionen einer der Aliase ist, die im Erweiterungsabschnitt definiert werden. Der Schnittstellentyp IBankAccount wird dem konkreten Typ BankAccount zugeordnet (die klassische Aufgabe eines DI-Containers) und mit einem bestimmten Abfangfunktionstyp verknüpft. Unity stellt zwei Haupttypen von Interceptors bereit: Instanzen-Interceptors und Typen-Interceptors. Im nächsten Monat werde ich die Interceptors ausführlicher behandeln. Zu diesem Zeitpunkt genügt es zu sagen, dass ein Instanzen-Interceptor einen Proxy für die Filterung eingehender Aufrufe erstellt, die an die abgefangene Instanz gerichtet sind. Typen-Interceptors ahmen lediglich den Typ des abgefangenen Objekts nach und bearbeiten eine Instanz eines abgeleiteten Typs. (Weitere Informationen zu Interceptors finden Sie unter msdn.microsoft.com/library/ff660861(PandP.20).)

Der Schnittstellen-Interceptor ist ein Instanzen-Interceptor, der auf die Funktion als Proxy einer einzelnen Schnittstelle auf dem Objekt beschränkt ist. Der Schnittstellen-Interceptor verwendet die dynamische Codegenerierung, um die Proxyklasse zu erstellen. Das Verhaltenselement der Abfangfunktion in der Konfiguration gibt den externen Code an, den Sie um die abgefangene Objektinstanz herum ausführen möchten. Die Klasse TraceBehavior muss deklarativ konfiguriert werden, sodass der Container diese und alle ihre Abhängigkeiten auflösen kann. Mittels des Elements <register> stellen Sie dem Container Informationen über die TraceBehavior-Klasse und dessen erwarteten Konstruktor bereit, wie im Folgenden gezeigt:

<register type="TraceBehavior"> 
   <constructor> 
     <param name="source" dependencyName="interception" /> 
   </constructor> 
</register>

Abbildung 2 zeigt einen Ausschnitt aus der TraceBehavior-Klasse.

Abbildung 2Ein Beispiel für ein Unity-Verhalten

class TraceBehavior : IInterceptionBehavior, IDisposable
{
  private TraceSource source;

  public TraceBehavior(TraceSource source)
  {
    if (source == null) 
      throw new ArgumentNullException("source");

    this.source = source;
  }
   
  public IEnumerable<Type> GetRequiredInterfaces()
  {
    return Type.EmptyTypes;
  }

  public IMethodReturn Invoke(IMethodInvocation input, 
    GetNextInterceptionBehaviorDelegate getNext)
  {
     // BEFORE the target method execution 
     this.source.TraceInformation("Invoking {0}",
       input.MethodBase.ToString());

     // Yield to the next module in the pipeline
     var methodReturn = getNext().Invoke(input, getNext);

     // AFTER the target method execution 
     if (methodReturn.Exception == null)
     {
       this.source.TraceInformation("Successfully finished {0}",
         input.MethodBase.ToString());
     }
     else
     {
       this.source.TraceInformation(
         "Finished {0} with exception {1}: {2}",
         input.MethodBase.ToString(),
         methodReturn.Exception.GetType().Name,
         methodReturn.Exception.Message);
     }

     this.source.Flush();
     return methodReturn;
   }

   public bool WillExecute
   {
     get { return true; }
   }

   public void Dispose()
   {
     this.source.Close();
   }
 }

Eine Verhaltensklasse implementiert IInterceptionBehavior, das im Wesentlichen aus der Invoke-Methode besteht. Die Invoke-Methode enthält die gesamte Logik, die Sie für die Methoden verwenden müssen, die durch den Interceptor gesteuert werden. Wenn Sie eine Aktion durchführen möchten, bevor die Zielmethode aufgerufen wird, müssen Sie dies zu Beginn der Methode tun. Wenn Sie zum Zielobjekt wechseln möchten – oder genauer gesagt, zum nächsten Verhalten, das in der Pipeline registriert ist – rufen Sie den getNext-Stellvertreter auf, der durch das Framework bereitgestellt wird. Sie können jeden gewünschten Code für die Nachbearbeitung des Zielobjekts verwenden. Die Invoke-Methode muss einen Verweis auf das nächste Element in der Pipeline zurückgeben. Wenn der Wert „Null“ zurückgegeben wird, ist die Kette unterbrochen und es werden keine weiteren Verhaltensweisen mehr aufgerufen.

Konfigurationsflexibilität

Die Abfangfunktion und allgemeiner ausgedrückt die AOP behandeln eine Reihe interessanter Szenarien. Die Abfangfunktion ermöglicht Ihnen zum Beispiel das Hinzufügen von Aufgaben zu einzelnen Objekten, ohne die gesamte Klasse zu modifizieren. Die Lösung bleibt sehr viel flexibler als bei Verwendung eines Decorator.

In diesem Artikel konnte die Anwendung der AOP auf .NET nur in Form einer Übersicht behandelt werden. In den nächsten Monaten werde ich mehr über die Abfangfunktion in Unity und AOP in Allgemeinen schreiben.

Dino Esposito ist Autor des Titels „Programming Microsoft ASP.NET MVC“ (Microsoft Press 2010) und Mitverfasser von „Microsoft .NET: Architecting Applications for the Enterprise“ (Microsoft Press 2008). Esposito lebt in Italien und ist ein weltweit gefragter Referent für Branchenveranstaltungen. Sie finden seinen Blog unter weblogs.asp.net/despos.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Chris Tavares