Aspektorientierte Programmierung

Aspektorientierte Programmierung mit der RealProxy-Klasse

Bruno Sonnino

Eine gut strukturierte Anwendung umfasst getrennte Schichten, damit die verschiedenen Belange nicht mehr als notwendig interagieren. Angenommen, Sie entwerfen eine locker gekoppelte, verwaltbare Anwendung, aber mitten in der Entwicklung erkennen Sie einige Anforderungen, die möglicherweise nicht gut in die Architektur passen, z. B.:

  • Die Anwendung benötigt ein Authentifizierungssystem, das vor jeder Abfrage oder Aktualisierung verwendet wird.
  • Die Daten müssen überprüft werden, bevor sie in die Datenbank geschrieben werden.
  • Die Anwendung muss mit Überwachungs- und Protokollierungsfunktionen für sensible Operationen ausgestattet sein.
  • Die Anwendung muss ein Debugprotokoll verwalten, um zu überprüfen, ob die Operationen in Ordnung sind.
  • Die Leistung einiger Operationen muss gemessen werden, um festzustellen, ob sie im gewünschten Bereich liegt.

Alle diese Anforderungen sind sehr arbeitsintensiv und erfordern zudem die Duplizierung von Code. Sie müssen denselben Code in vielen Teilen des Systems hinzufügen, was dem Wiederhole-dich-nicht-Prinzip (Don't Repeat Yourself, DRY) widerspricht und die Wartung erschwert. Jede Änderung einer Anforderung führt zu einer umfangreichen Änderung im Programm. Wenn ich in meinen Anwendungen so etwas hinzufügen muss, denke ich „Warum kann der Compiler den wiederholten Code nicht an mehreren Stellen für mich hinzufügen?“ oder „Ich wünschte, es gäbe eine Option, um dieser Methode eine Protokollierungsfunktion hinzuzufügen.“.

Die gute Nachricht ist, dass es so etwas gibt, nämlich die aspektorientierte Programmierung (AOP). Sie trennt allgemeinen Code von Aspekten, die über die Grenzen eines Objekts oder einer Schicht hinausgehen. Das Anwendungsprotokoll ist beispielsweise nicht an eine Anwendungsschicht gebunden. Es wird auf das gesamte Programm angewendet und sollte überall präsent sein. Das wird als Cross-Cutting Concern (querschnittlicher Belang) bezeichnet.

Laut Wikipedia ist die aspektorientierte Programmierung (AOP) ein Programmierparadigma für die objektorientierte Programmierung, um generische Funktionalitäten über mehrere Klassen hinweg zu verwenden (Cross-Cutting Concern). Die AOP befasst sich mit der Funktionalität, die in mehreren Teilen des Systems vorkommt, trennt sie vom Kern der Anwendung, sorgt so für eine bessere Trennung der Belange und vermeidet gleichzeitig eine Duplizierung von Code und Kopplung.

In diesem Artikel erkläre ich die Grundlagen der AOP und beschreibe dann genau, wie sie durch Verwendung eines dynamischen Proxys über die Microsoft .NET Framework-Klasse „RealProxy“ vereinfacht wird.

Implementieren der AOP

Der größte Vorteil der AOP besteht darin, dass Sie sich nur an einer Stelle um einen Aspekt Gedanken machen müssen, ihn einmal programmieren und dann an allen notwendigen Stellen anwenden. Es gibt viele Einsatzmöglichkeiten für die AOP, wie etwa:

  • Implementieren der Protokollierung in eine Anwendung
  • Verwenden der Authentifizierung vor einer Operation (Beispielsweise können einige Operationen nur authentifizierten Benutzern ermöglicht werden.)
  • Implementieren der Prüfung oder Benachrichtigung für Eigenschaftensetter (die das PropertyChanged-Ereignis aufrufen, wenn eine Eigenschaft für Klassen geändert wurde, die die INotifyPropertyChanged-Schnittstelle implementieren)
  • Ändern des Verhaltens einiger Methoden

Wie Sie sehen, ist die AOP vielseitig einsetzbar, bei ihrer Verwendung ist jedoch Sorgfalt geboten. Ein Teil des Codes bleibt Ihnen verborgen. Er ist aber dennoch da und wird in jedem Aufruf ausgeführt, der den jeweiligen Aspekt enthält. Der Code kann Fehler enthalten und die Anwendungsleistung erheblich beeinträchtigen. Ein kleiner Fehler in einem Aspekt kann bedeuten, dass Sie viele Stunden für das Debuggen aufwenden müssen. Wenn ein Aspekt nur an wenigen Stellen verwendet wird, ist es manchmal besser, ihn direkt zum Code hinzuzufügen.

AOP-Implementierungen verwenden einige gängige Techniken:

  • Hinzufügen von Quellcode mithilfe eines Präprozessors (z. B. der Präprozessor aus C++)
  • Verwenden eines Postprozessors zum Hinzufügen von Anweisungen zum kompilierten binären Code
  • Verwenden eines besonderen Compilers, der den Code während der Kompilierung hinzufügt
  • Verwenden eines Code-Interceptors zur Laufzeit, der die Ausführung abfängt und den gewünschten Code hinzufügt

In .NET Framework werden von diesen Techniken meistens die Nachverarbeitung und das Abfangen von Code verwendet. Die erste Technik wird von PostSharp (postsharp.net) eingesetzt und die letzte von Abhängigkeitsinjektion-Containern (DI-Container) wie Castle DynamicProxy (bit.ly/JzE631) und Unity (unity.codeplex.com). Diese Tools verwenden zum Abfangen von Code in der Regel ein Entwurfsmuster namens „Decorator“ oder „Proxy“.

Das Decorator-Entwurfsmuster

Das Decorator-Entwurfsmuster löst ein allgemeines Problem: Sie haben eine Klasse und möchten ihr Funktionalität hinzufügen. Hierzu stehen Ihnen verschiedene Möglichkeiten zur Verfügung:

  • Sie könnten der Klasse die neue Funktionalität direkt hinzufügen. Hierdurch erhält die Klasse jedoch eine weitere Verantwortung, was einen Verstoß gegen das Prinzip der einzigen Verantwortung darstellt.
  • Sie könnten eine neue Klasse erstellen, die diese Funktionalität ausführt und sie über die alte Klasse aufrufen. Das wirft ein neues Problem auf: Was ist, wenn Sie die Klasse auch ohne die neue Funktionalität verwenden möchten?
  • Sie könnten eine neue Klasse erben und die neue Funktionalität hinzufügen. Hierdurch könnten jedoch viele neue Klassen entstehen. Angenommen, Sie haben eine Repositoryklasse für CRUD-Datenbankoperationen (Create, Read, Update und Delete – Erstellen, Lesen, Aktualisieren und Löschen) und möchten die Überwachung hinzufügen. Später möchten Sie die Datenüberprüfung hinzufügen, um die ordnungsgemäße Aktualisierung der Daten zu gewährleisten. Dann möchten Sie möglicherweise auch den Zugriff authentifizieren, um sicherzustellen, dass nur autorisierte Benutzer auf die Klassen zugreifen können. Das stellt Sie vor große Probleme: Sie könnten einige Klassen haben, die alle drei Aspekte implementieren, und einige, die nur zwei Aspekte oder sogar nur einen implementieren. Aber wie viele Klassen hätten Sie dann letztendlich?
  • Sie können die Klasse mit dem Aspekt „dekorieren“, wodurch eine neue Klasse erstellt wird, die den Aspekt verwendet und dann die alte Klasse aufruft. Wenn Sie einen Aspekt benötigen, dekorieren Sie ihn demnach einmal. Sind zwei Aspekte erforderlich, dekorieren Sie ihn zweimal usw. Angenommen, Sie bestellen ein Spielzeug. (Da wir alle Computerfreaks sind, nehmen wir eine Xbox oder ein Smartphone.) Für die Ausstellung im Geschäft und zum Schutz ist eine Verpackung erforderlich. Sie bestellen es dann noch mit einer Geschenkverpackung (die zweite Dekoration), um den Karton mit Geschenkpapier, Klebestreifen, Geschenkband und einer Karte zu verschönern. Das Geschäft verschickt das Spielzeug in einer dritten Verpackung, einem Karton mit Styroporkugeln, die zum Schutz dienen. Sie haben drei Dekorationen, die alle eine andere Funktionalität haben und unabhängig voneinander sind. Sie können das Spielzeug ohne Geschenkverpackung erwerben oder es im Geschäft ohne den äußeren Karton bzw. ganz ohne Karton kaufen (mit Sonderrabatt!). Das Spielzeug kann mit jeder beliebigen Kombination von Dekorationen versehen werden, an seiner grundlegenden Funktionalität ändert sich nichts.

Nachdem Sie etwas über das Decorator-Muster erfahren haben, zeige ich Ihnen, wie es in C# implementiert wird.

Zunächst erstellen Sie eine IRepository<T>-Schnittstelle:

public interface IRepository<T>
{
  void Add(T entity);
  void Delete(T entity);
  void Update(T entity);
  IEnumerable<T> GetAll();
  T GetById(int id);
}

Implementieren Sie sie mit der Repository<T>-Klasse (siehe Abbildung 1).

Abbildung 1: Die Repository<T>-Klasse

public class Repository<T> : IRepository<T>
{
  public void Add(T entity)
  {
    Console.WriteLine("Adding {0}", entity);
  }
  public void Delete(T entity)
  {
    Console.WriteLine("Deleting {0}", entity);
  }
  public void Update(T entity)
  {
    Console.WriteLine("Updating {0}", entity);
  }
  public IEnumerable<T> GetAll()
  {
    Console.WriteLine("Getting entities");
    return null;
  }
  public T GetById(int id)
  {
    Console.WriteLine("Getting entity {0}", id);
    return default(T);
  }
}

Verwenden Sie die Repository<T>-Klasse, um die Elemente der Customer-Klasse hinzuzufügen, zu aktualisieren, zu löschen und abzurufen:

public class Customer
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string Address { get; set; }
}

Das Programm sieht in etwa so wie in Abbildung 2 aus.

Abbildung 2: Das Hauptprogramm mit „no logging“

static void Main(string[] args)
{
  Console.WriteLine("***\r\n Begin program - no logging\r\n");
  IRepository<Customer> customerRepository =
    new Repository<Customer>();
  var customer = new Customer
  {
    Id = 1,
    Name = "Customer 1",
    Address = "Address 1"
  };
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine("\r\nEnd program - no logging\r\n***");
  Console.ReadLine();
}

Wenn Sie diesen Code ausführen, sehen Sie etwas Ähnliches wie in Abbildung 3.

Output of the Program with No Logging
Abbildung 3: Ausgabe des Programms mit „no logging“

Stellen Sie sich vor, Ihr Vorgesetzter bittet Sie, dieser Klasse eine Protokollierungsfunktion hinzuzufügen. Sie können eine neue Klasse erstellen, die „IRepository<T>“ dekoriert. Sie erhält die zu erstellende Klasse und implementiert dieselbe Schnittstelle (siehe Abbildung 4).

Abbildung 4: „LoggerRepository“

public class LoggerRepository<T> : IRepository<T>
{
  private readonly IRepository<T> _decorated;
  public LoggerRepository(IRepository<T> decorated)
  {
    _decorated = decorated;
  }
  private void Log(string msg, object arg = null)
  {
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(msg, arg);
    Console.ResetColor();
  }
  public void Add(T entity)
  {
    Log("In decorator - Before Adding {0}", entity);
    _decorated.Add(entity);
    Log("In decorator - After Adding {0}", entity);
  }
  public void Delete(T entity)
  {
    Log("In decorator - Before Deleting {0}", entity);
    _decorated.Delete(entity);
    Log("In decorator - After Deleting {0}", entity);
  }
  public void Update(T entity)
  {
    Log("In decorator - Before Updating {0}", entity);
    _decorated.Update(entity);
    Log("In decorator - After Updating {0}", entity);
  }
  public IEnumerable<T> GetAll()
  {
    Log("In decorator - Before Getting Entities");
    var result = _decorated.GetAll();
    Log("In decorator - After Getting Entities");
    return result;
  }
  public T GetById(int id)
  {
    Log("In decorator - Before Getting Entity {0}", id);
    var result = _decorated.GetById(id);
    Log("In decorator - After Getting Entity {0}", id);
    return result;
  }
}

Diese neue Klasse umschließt die Methoden für die dekorierte Klasse und fügt das Protokollierungsfeature hinzu. Sie müssen den Code zum Aufrufen der Logging-Klasse ein wenig ändern (siehe Abbildung 5).

Abbildung 5: Das Hauptprogramm verwendet „LoggerRepository“

static void Main(string[] args)
{
  Console.WriteLine("***\r\n Begin program - logging with decorator\r\n");
  // IRepository<Customer> customerRepository =
  //   new Repository<Customer>();
  IRepository<Customer> customerRepository =
    new LoggerRepository<Customer>(new Repository<Customer>());
  var customer = new Customer
  {
    Id = 1,
    Name = "Customer 1",
    Address = "Address 1"
  };
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine("\r\nEnd program - logging with decorator\r\n***");
  Console.ReadLine();
}

Sie erstellen einfach die neue Klasse, wobei eine Instanz der alten Klasse als Parameter für ihren Konstruktor übergeben wird. Wenn Sie das Programm ausführen, sehen Sie, dass es mit der Protokollierungsfunktion ausgestattet ist (siehe Abbildung 6).

Execution of the Logging Program with a Decorator
Abbildung 6: Ausführung des Protokollierungsprogramms mit einem Decorator

Vielleicht denken Sie jetzt Folgendes: „Die Idee ist gut, aber es ist viel Arbeit: Ich muss alle Klassen implementieren und den Aspekt zu allen Methoden hinzufügen. Das ist schwer zu verwalten. Geht das nicht anders?“ Mit .NET Framework können Sie die Reflektion verwenden, um alle Methoden abzurufen und auszuführen. Die Basisklassenbibliothek (BCL) verfügt sogar über die RealProxy-Klasse (bit.ly/18MfxWo), die die Implementierung für Sie übernimmt.

Erstellen eines dynamischen Proxys mit „RealProxy“

Durch die RealProxy-Klasse erhalten Sie die Basisfunktionalität für Proxys. Es handelt sich um eine abstrakte Klasse, die durch Überschreiben ihrer Invoke-Methode und Hinzufügen neuer Funktionen geerbt werden muss. Diese Klasse befindet sich im System.Runtime.Remoting.Proxies-Namespace. Zum Erstellen eines dynamischen Proxys verwenden Sie Code wie in Abbildung 7.

Abbildung 7: Die DynamicProxy-Klasse

class DynamicProxy<T> : RealProxy
{
  private readonly T _decorated;
  public DynamicProxy(T decorated)
    : base(typeof(T))
  {
    _decorated = decorated;
  }
  private void Log(string msg, object arg = null)
  {
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(msg, arg);
    Console.ResetColor();
  }
  public override IMessage Invoke(IMessage msg)
  {
    var methodCall = msg as IMethodCallMessage;
    var methodInfo = methodCall.MethodBase as MethodInfo;
    Log("In Dynamic Proxy - Before executing '{0}'",
      methodCall.MethodName);
    try
    {
      var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
      Log("In Dynamic Proxy - After executing '{0}' ",
        methodCall.MethodName);
      return new ReturnMessage(result, null, 0,
        methodCall.LogicalCallContext, methodCall);
    }
    catch (Exception e)
    {
     Log(string.Format(
       "In Dynamic Proxy- Exception {0} executing '{1}'", e),
       methodCall.MethodName);
     return new ReturnMessage(e, methodCall);
    }
  }
}

Im Konstruktor der Klasse müssen Sie den Konstruktor der Basisklasse aufrufen, wobei der Typ der zu dekorierenden Klasse übergeben wird. Dann müssen Sie die Invoke-Methode überschreiben, die einen IMessage-Parameter erhält. Sie enthält ein Wörterbuch mit allen für die Methode übergebenen Parametern. Der IMessage-Parameter wird als „IMethodCallMessage“ typisiert, sodass Sie den MethodBase-Parameter extrahieren können (der den MethodInfo-Typ aufweist).

Die nächsten Schritte bestehen darin, den gewünschten Aspekt vor dem Aufrufen der Methode hinzuzufügen, die ursprüngliche Methode mit „methodInfo.Invoke“ aufzurufen und den Aspekt dann nach dem Aufruf hinzuzufügen.

Sie können den Proxy nicht direkt aufrufen, da „DynamicProxy<T>“ kein „IRepository<Customer>“ ist. Das heißt, dass Sie ihn so nicht aufrufen können:

IRepository<Customer> customerRepository =
  new DynamicProxy<IRepository<Customer>>(
  new Repository<Customer>());

Wenn Sie das dekorierte Repository verwenden möchten, müssen Sie die GetTransparentProxy-Methode verwenden, die eine IRepository<Customer>-Instanz zurückgibt. Jede Methode dieser Instanz, die aufgerufen wird, muss die Invoke-Methode des Proxys durchlaufen. Zur Vereinfachung dieses Prozesses können Sie eine Factory-Klasse anlegen, um den Proxy zu erstellen und die Instanz für das Repository zurückzugeben:

public class RepositoryFactory
{
  public static IRepository<T> Create<T>()
  {
    var repository = new Repository<T>();
    var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
    return dynamicProxy.GetTransparentProxy() as IRepository<T>;
  }
}

Das Hauptprogramm sieht dann ähnlich aus wie in Abbildung 8.

Abbildung 8: Das Hauptprogramm mit einem dynamischen Proxy

static void Main(string[] args)
{
  Console.WriteLine("***\r\n Begin program - logging with dynamic proxy\r\n");
  // IRepository<Customer> customerRepository =
  //   new Repository<Customer>();
  // IRepository<Customer> customerRepository =
  //   new LoggerRepository<Customer>(new Repository<Customer>());
  IRepository<Customer> customerRepository =
    RepositoryFactory.Create<Customer>();
  var customer = new Customer
  {
    Id = 1,
    Name = "Customer 1",
    Address = "Address 1"
   ;
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine("\r\nEnd program - logging with dynamic proxy\r\n***");
  Console.ReadLine();
}

Wenn Sie dieses Programm ausführen, erhalten Sie ein ähnliches Ergebnis wie zuvor (siehe Abbildung 9).

Program Execution with Dynamic Proxy
Abbildung 9: Programmausführung mit dynamischem Proxy

Wie Sie sehen, haben Sie einen dynamischen Proxy erstellt, mit dem dem Code Aspekte hinzugefügt werden können, ohne dass er wiederholt werden muss. Wenn Sie einen neuen Aspekt hinzufügen möchten, müssen Sie nur eine neue Klasse erstellen, von „RealProxy“ erben und sie zum Dekorieren des ersten Proxys verwenden.

Wenn Ihr Vorgesetzter wieder zu Ihnen kommt und Sie bittet, dem Code eine Autorisierungsfunktion hinzuzufügen, damit nur Administratoren auf das Repository zugreifen können, können Sie einen neuen Proxy erstellen (siehe Abbildung 10).

Abbildung 10: Ein Authentifizierungsproxy

class AuthenticationProxy<T> : RealProxy
{
  private readonly T _decorated;
  public AuthenticationProxy(T decorated)
    : base(typeof(T))
  {
    _decorated = decorated;
  }
  private void Log(string msg, object arg = null)
  {
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine(msg, arg);
    Console.ResetColor();
  }
  public override IMessage Invoke(IMessage msg)
  {
    var methodCall = msg as IMethodCallMessage;
    var methodInfo = methodCall.MethodBase as MethodInfo;
    if (Thread.CurrentPrincipal.IsInRole("ADMIN"))
    {
      try
      {
        Log("User authenticated - You can execute '{0}' ",
          methodCall.MethodName);
        var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
        return new ReturnMessage(result, null, 0,
          methodCall.LogicalCallContext, methodCall);
      }
      catch (Exception e)
      {
        Log(string.Format(
          "User authenticated - Exception {0} executing '{1}'", e),
          methodCall.MethodName);
        return new ReturnMessage(e, methodCall);
      }
    }
    Log("User not authenticated - You can't execute '{0}' ",
      methodCall.MethodName);
    return new ReturnMessage(null, null, 0,
      methodCall.LogicalCallContext, methodCall);
  }
}

Die Repository-Factory muss geändert werden, damit beide Proxys aufgerufen werden (siehe Abbildung 11).

Abbildung 11: Die mit zwei Proxys dekorierte Repository-Factory

public class RepositoryFactory
{
  public static IRepository<T> Create<T>()
  {
    var repository = new Repository<T>();
    var decoratedRepository =
      (IRepository<T>)new DynamicProxy<IRepository<T>>(
      repository).GetTransparentProxy();
    // Create a dynamic proxy for the class already decorated
    decoratedRepository =
      (IRepository<T>)new AuthenticationProxy<IRepository<T>>(
      decoratedRepository).GetTransparentProxy();
    return decoratedRepository;
  }
}

Wenn Sie das Hauptprogramm in das in Abbildung 12 gezeigte Programm ändern und ausführen, erhalten Sie die in Abbildung 13 dargestellte Ausgabe.

Abbildung 12: Das Hauptprogramm ruft das Repository mit zwei Benutzern auf

static void Main(string[] args)
{
  Console.WriteLine(
    "***\r\n Begin program - logging and authentication\r\n");
  Console.WriteLine("\r\nRunning as admin");
  Thread.CurrentPrincipal =
    new GenericPrincipal(new GenericIdentity("Administrator"),
    new[] { "ADMIN" });
  IRepository<Customer> customerRepository =
    RepositoryFactory.Create<Customer>();
  var customer = new Customer
  {
    Id = 1,
    Name = "Customer 1",
    Address = "Address 1"
  };
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine("\r\nRunning as user");
  Thread.CurrentPrincipal =
    new GenericPrincipal(new GenericIdentity("NormalUser"),
    new string[] { });
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine(
    "\r\nEnd program - logging and authentication\r\n***");
  Console.ReadLine();
}

Output of the Program Using Two Proxies
Abbildung 13: Ausgabe des Programms, das zwei Proxys verwendet

Das Programm führt die Repositorymethoden zweimal aus. Beim ersten Mal wird es als Administratorbenutzer ausgeführt, und die Methoden werden aufgerufen. Beim zweiten Mal wird es als normaler Benutzer ausgeführt, und die Methoden werden übersprungen.

Das ist viel einfacher, oder? Beachten Sie, dass die Factory eine IRepository<T>-Instanz zurückgibt, sodass das Programm nicht weiß, ob es die dekorierte Version verwendet. Hier wird das Liskovsche Substitutionsprinzip berücksichtigt, das Folgendes besagt: Wenn S ein Untertyp von T ist, können Objekte des Typs T durch Objekte des Typs S ersetzt werden. In diesem Fall ermöglicht die Verwendung einer IRepository<Customer>-Schnittstelle es Ihnen, jede Klasse zu verwenden, die diese Schnittstelle implementiert, ohne das Programm zu ändern.

Filterfunktionen

Bisher gibt es keine Filterfunktionen. Der Aspekt wird auf jede Klassenmethode angewendet, die aufgerufen wird. Oft ist dieses Verhalten aber nicht erwünscht. Angenommen, die Abrufmethoden („GetAll“ und „GetById“) sollen nicht protokolliert werden. Eine Möglichkeit besteht darin, den Aspekt nach Namen zu filtern (siehe Abbildung 14).

Abbildung 14: Filtermethoden für den Aspekt

public override IMessage Invoke(IMessage msg)
{
  var methodCall = msg as IMethodCallMessage;
  var methodInfo = methodCall.MethodBase as MethodInfo;
  if (!methodInfo.Name.StartsWith("Get"))
    Log("In Dynamic Proxy - Before executing '{0}'",
      methodCall.MethodName);
  try
  {
    var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
    if (!methodInfo.Name.StartsWith("Get"))
      Log("In Dynamic Proxy - After executing '{0}' ",
        methodCall.MethodName);
      return new ReturnMessage(result, null, 0,
       methodCall.LogicalCallContext, methodCall);
  }
  catch (Exception e)
  {
    if (!methodInfo.Name.StartsWith("Get"))
      Log(string.Format(
        "In Dynamic Proxy- Exception {0} executing '{1}'", e),
        methodCall.MethodName);
      return new ReturnMessage(e, methodCall);
  }
}

Das Programm prüft, ob die Methode mit „Get“ beginnt. Ist dies der Fall, wendet das Programm den Aspekt nicht an. Das funktioniert, aber der Filtercode wird dreimal wiederholt. Zudem befindet sich der Filter innerhalb des Proxys, weshalb Sie jedes Mal die Klasse ändern müssen, wenn Sie den Proxy ändern möchten. Eine Verbesserung erreichen Sie durch die Erstellung eines IsValidMethod-Prädikats:

private static bool IsValidMethod(MethodInfo methodInfo)
{
  return !methodInfo.Name.StartsWith("Get");
}

Jetzt müssen Sie die Änderung nur an einer Stelle vornehmen, jedoch müssen Sie immer noch jedes Mal die Klasse ändern, wenn Sie den Filter ändern möchten. Die Lösung besteht darin, den Filter als Klasseneigenschaft verfügbar zu machen, sodass Sie dem Aufrufer die Verantwortung für die Erstellung eines Filters zuweisen. Sie können eine Filter-Eigenschaft des Predicate<MethodInfo>-Typs erstellen und sie zum Filtern der Daten verwenden (siehe Abbildung 15).

Abbildung 15: Ein Filterproxy

class DynamicProxy<T> : RealProxy
{
  private readonly T _decorated;
  private Predicate<MethodInfo> _filter;
  public DynamicProxy(T decorated)
    : base(typeof(T))
  {
    _decorated = decorated;
    _filter = m => true;
  }
  public Predicate<MethodInfo> Filter
  {
    get { return _filter; }
    set
    {
      if (value == null)
        _filter = m => true;
      else
        _filter = value;
    }
  }
  private void Log(string msg, object arg = null)
  {
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(msg, arg);
    Console.ResetColor();
  }
  public override IMessage Invoke(IMessage msg)
  {
    var methodCall = msg as IMethodCallMessage;
    var methodInfo = methodCall.MethodBase as MethodInfo;
    if (_filter(methodInfo))
      Log("In Dynamic Proxy - Before executing '{0}'",
        methodCall.MethodName);
    try
    {
      var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
      if (_filter(methodInfo))
        Log("In Dynamic Proxy - After executing '{0}' ",
          methodCall.MethodName);
        return new ReturnMessage(result, null, 0,
          methodCall.LogicalCallContext, methodCall);
    }
    catch (Exception e)
    {
      if (_filter(methodInfo))
        Log(string.Format(
          "In Dynamic Proxy- Exception {0} executing '{1}'", e),
          methodCall.MethodName);
      return new ReturnMessage(e, methodCall);
    }
  }
}

Die Filter-Eigenschaft wird mit „Filter = m => true“ initialisiert. Das bedeutet, dass kein Filter aktiv ist. Beim Zuweisen der Filter-Eigenschaft überprüft das Programm, ob der Wert Null ist. Ist dies der Fall, setzt es den Filter zurück. Bei der Ausführung der Invoke-Methode prüft das Programm das Filterergebnis. Lautet es „true“, wendet es den Aspekt an. Jetzt sieht die Proxyerstellung in der Factoryklasse so aus:

public class RepositoryFactory
{
  public static IRepository<T> Create<T>()
  {
    var repository = new Repository<T>();
    var dynamicProxy = new DynamicProxy<IRepository<T>>(repository)
    {
      Filter = m => !m.Name.StartsWith("Get")
      };
      return dynamicProxy.GetTransparentProxy() as IRepository<T>;
    }  
  }
}

Die Verantwortung für die Filtererstellung wurde auf die Factory übertragen. Wenn Sie das Programm ausführen, sollte das in etwa wie in Abbildung 16 aussehen.

Output with a Filtered Proxy
Abbildung 16: Ausgabe mit einem gefilterten Proxy

Beachten Sie, dass die letzten beiden Methoden „GetAll“ und „GetById“ (repräsentiert durch „Getting entities“ und „Getting entity 1“) in Abbildung 16 nicht von Protokollierungsfunktionen umschlossen sind. Sie können die Klassen sogar noch weiter verbessern, indem Sie die Aspekte als Ereignisse verfügbar machen. Sie brauchen dann die Klasse nicht jedes Mal ändern, wenn Sie den Aspekt ändern möchten. Dies wird in Abbildung 17 gezeigt.

Abbildung 17: Ein flexibler Proxy

class DynamicProxy<T> : RealProxy
{
  private readonly T _decorated;
  private Predicate<MethodInfo> _filter;
  public event EventHandler<IMethodCallMessage> BeforeExecute;
  public event EventHandler<IMethodCallMessage> AfterExecute;
  public event EventHandler<IMethodCallMessage> ErrorExecuting;
  public DynamicProxy(T decorated)
    : base(typeof(T))
  {
    _decorated = decorated;
    Filter = m => true;
  }
  public Predicate<MethodInfo> Filter
  {
    get { return _filter; }
    set
    {
      if (value == null)
        _filter = m => true;
      else
        _filter = value;
    }
  }
  private void OnBeforeExecute(IMethodCallMessage methodCall)
  {
    if (BeforeExecute != null)
    {
      var methodInfo = methodCall.MethodBase as MethodInfo;
      if (_filter(methodInfo))
        BeforeExecute(this, methodCall);
    }
  }
  private void OnAfterExecute(IMethodCallMessage methodCall)
  {
    if (AfterExecute != null)
    {
      var methodInfo = methodCall.MethodBase as MethodInfo;
      if (_filter(methodInfo))
        AfterExecute(this, methodCall);
    }
  }
  private void OnErrorExecuting(IMethodCallMessage methodCall)
  {
    if (ErrorExecuting != null)
    {
      var methodInfo = methodCall.MethodBase as MethodInfo;
      if (_filter(methodInfo))
        ErrorExecuting(this, methodCall);
    }
  }
  public override IMessage Invoke(IMessage msg)
  {
    var methodCall = msg as IMethodCallMessage;
    var methodInfo = methodCall.MethodBase as MethodInfo;
    OnBeforeExecute(methodCall);
    try
    {
      var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
      OnAfterExecute(methodCall);
      return new ReturnMessage(
        result, null, 0, methodCall.LogicalCallContext, methodCall);
    }
    catch (Exception e)
    {
      OnErrorExecuting(methodCall);
      return new ReturnMessage(e, methodCall);
    }
  }
}

In Abbildung 17 werden die drei Ereignisse „BeforeExecute“, „AfterExecute“ und „ErrorExecuting“ von den Methoden „OnBeforeExecute“, „OnAfterExecute“ und „OnErrorExecuting“ aufgerufen. Diese Methoden überprüfen, ob der Ereignishandler definiert ist. Ist dies der Fall, prüfen sie, ob die aufgerufene Methode den Filter übergibt. Wenn ja, rufen sie den Ereignishandler auf, der den Aspekt anwendet. Die Factoryklasse sieht jetzt in etwa so wie in Abbildung 18 aus.

Abbildung 18: Eine Repository-Factory, die Aspektereignisse und den Filter festlegt

public class RepositoryFactory
{
  private static void Log(string msg, object arg = null)
  {
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(msg, arg);
    Console.ResetColor();
  }
  public static IRepository<T> Create<T>()
  {
    var repository = new Repository<T>();
    var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
    dynamicProxy.BeforeExecute += (s, e) => Log(
      "Before executing '{0}'", e.MethodName);
    dynamicProxy.AfterExecute += (s, e) => Log(
      "After executing '{0}'", e.MethodName);
    dynamicProxy.ErrorExecuting += (s, e) => Log(
      "Error executing '{0}'", e.MethodName);
    dynamicProxy.Filter = m => !m.Name.StartsWith("Get");
    return dynamicProxy.GetTransparentProxy() as IRepository<T>;
  }
}

Jetzt haben Sie eine flexible Proxyklasse und können sich entscheiden, die Aspekte vor der Ausführung, nach der Ausführung oder bei Auftreten eines Fehlers anzuwenden, und zwar nur für ausgewählte Methoden. So können Sie viele Aspekte im Code auf einfache Weise ohne Wiederholung anwenden.

Kein Ersatz

Mit der AOP können Sie allen Anwendungsschichten Code zentral hinzufügen, ohne Code wiederholen zu müssen. Ich habe Ihnen gezeigt, wie auf Grundlage des Decorator-Entwurfsmusters ein generischer dynamischer Proxy erstellt wird, der mithilfe von Ereignissen und eines Prädikats zum Filtern der gewünschten Funktionen Aspekte auf Ihre Klassen anwendet.

Wie Sie sehen, ist die RealProxy-Klasse eine flexible Klasse, die Ihnen die vollständige Kontrolle über den Code gibt – ohne externe Abhängigkeiten. „RealProxy“ ist jedoch kein Ersatz für andere AOP-Tools, wie z. B. PostSharp. PostSharp verwendet eine ganz andere Methode. Dieses Tool fügt IL-Code (Intermediate Language) in einem Schritt nach der Kompilierung hinzu und verwendet keine Reflektion. Seine Leistung ist daher besser als die von „RealProxy“. Die Implementierung eines Aspekts mit „RealProxy“ ist zudem aufwendiger als mit PostSharp. Mit PostSharp müssen Sie nur die Aspektklasse erstellen und der Klasse (oder der Methode) dort ein Attribut hinzufügen, wo der Aspekt hinzugefügt werden soll – das ist alles.

Mit „RealProxy“ haben Sie andererseits die volle Kontrolle über den Quellcode. Es gibt keine externen Abhängigkeiten, und Sie können den Code nach Bedarf erweitern und anpassen. Wenn Sie beispielsweise einen Aspekt nur auf Methoden anwenden möchten, die das Log-Attribut aufweisen, könnten Sie so vorgehen:

public override IMessage Invoke(IMessage msg)
{
  var methodCall = msg as IMethodCallMessage;
  var methodInfo = methodCall.MethodBase as MethodInfo;
  if (!methodInfo.CustomAttributes
    .Any(a => a.AttributeType == typeof (LogAttribute)))
  {
    var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
    return new ReturnMessage(result, null, 0,
      methodCall.LogicalCallContext, methodCall);
  }
    ...

Außerdem ist die von „RealProxy“ verwendete Technik (Code abfangen und es dem Programm ermöglichen, ihn zu ersetzen) sehr leistungsstark. Wenn Sie beispielsweise zu Testzwecken ein Mockframework für generische Mocks und Stubs erstellen möchten, können Sie mithilfe der RealProxy-Klasse alle Aufrufe abfangen und sie durch eigenes Verhalten ersetzen. Aber das ist ein Thema für einen anderen Artikel!

Bruno Sonnino ist ein Microsoft Most Valuable Professional (MVP), der in Brasilien lebt. Er ist als Entwickler, Berater und Autor tätig, hat fünf Bücher über Delphi geschrieben, die bei Pearson Education Brazil auf Portugiesisch erschienen sind, sowie zahlreiche Artikel für brasilianische und US-amerikanische Magazine und Websites verfasst.

Unser Dank gilt den folgenden technischen Experten von Microsoft Research für die Durchsicht dieses Artikels: James McCaffrey, Carlos Suarez und Johan Verwey
James McCaffrey arbeitet für Microsoft Research in Redmond, Washington, USA. Er hat an verschiedenen Microsoft-Produkten mitgearbeitet, unter anderem an Internet Explorer und Bing. Sie erreichen ihn unter jammc@microsoft.com.

Carlos Garcia Jurado Suarez ist Research Software Engineer bei Microsoft Research. Er gehörte zum Advanced Development Team und arbeitet in jüngster Zeit in der Machine Learning Group. Früher war er Visual Studio-Entwickler und arbeitete an Modellierungstools wie dem Klassen-Designer.