Cet article a fait l'objet d'une traduction automatique.
La programmation orientée aspect
La programmation orientée aspect avec la classe RealProxy
Une application bien architecture a des calques distincts pour différentes préoccupations n'interagissent plus que nécessaire. Imaginons que vous concevez une application faiblement couplée et facile à gérer, mais au milieu de la mise au point, vous verrez certaines exigences qui pourraient convenir pas bien dans l'architecture, telles que :
- L'application doit posséder un système d'authentification utilisé avant toute requête ou de la mise à jour.
- Les données doivent être validées avant qu'il est écrit dans la base de données.
- L'application doit avoir audit et journalisation des opérations sensibles.
- L'application doit conserver un journal de débogage pour vérifier si les opérations sont OK.
- Certaines opérations doivent avoir leurs performances mesurées pour voir si elles sont dans la plage désirée.
Toutes ces conditions ont besoin de beaucoup de travail et, plus que cela, duplication de code. Vous devez ajouter le code même dans de nombreuses parties du système, qui va à l'encontre de la « ne vous répétez » principe (DRY) et complique l'entretien. Toute modification de l'exigence provoque un changement massif dans le programme. Quand je dois ajouter quelque chose comme ça dans mes applications, je pense, « pourquoi ne peut pas le compilateur ajoute cela répété code à plusieurs endroits pour moi? » ou, « Je voudrais avoir une option pour « Add me connecter à cette méthode. » »
La bonne nouvelle, c'est que quelque chose comme qui n'existe pas : programmation orientée aspect (AOP). Il sépare le code général des aspects qui traversent les limites d'un objet ou une couche. Par exemple, le journal des applications n'est pas lié à n'importe quelle couche de l'application. Il s'applique à l'ensemble du programme et devrait être présent partout. Cela s'appelle une préoccupation transversale.
AOP est, selon Wikipedia, « un paradigme de programmation qui vise à accroître la modularité en permettant la séparation des coupes transversales de préoccupations. » Il traite des fonctionnalités qui se produit dans plusieurs parties du système et la sépare de la base de l'application, afin d'améliorer la séparation des préoccupations tout en évitant la duplication de code et de couplage.
Dans cet article, je vais expliquer les bases de la POA et ensuite en détail comment faire pour rendre plus facile en utilisant un proxy dynamique via la classe Microsoft .NET Framework RealProxy.
Mise en œuvre des AOP
Le plus grand avantage de la POA, c'est que vous n'avez à vous soucier de l'aspect en un seul endroit, programmant une fois et l'appliquer dans tous les endroits où nécessaire. Il y a beaucoup d'utilisations pour les AOP, tels que :
- D'application de journalisation à votre application.
- L'authentification avant une opération (par exemple de permettre certaines opérations uniquement pour les utilisateurs authentifiés).
- Mise en œuvre de validation ou la notification pour les accesseurs set de propriété (appeler l'événement PropertyChanged lorsqu'une propriété a été modifiée pour les classes qui implémentent l'interface INotifyPropertyChanged).
- Modifier le comportement de certaines méthodes.
Comme vous pouvez le voir, AOP a de nombreux usages, mais vous il devez manier avec précaution. Elle permet de conserver un code hors de votre vue, mais il est toujours là, en cours d'exécution dans chaque appel où l'aspect est présent. Il peut avoir des bugs et gravement affecter les performances de l'application. Un bogue subtil dans l'aspect pourrait vous coûter de nombreuses heures de débogage. Si votre aspect n'est pas utilisé dans de nombreux endroits, il est parfois préférable de l'ajouter directement dans le code.
AOP implémentations utilisent certaines techniques courantes :
- Ajout de code source en utilisant un pré-processeur, telle que celle en C++.
- En utilisant un post-processeur pour ajouter des instructions sur le code binaire compilé.
- À l'aide d'un compilateur spécial qui ajoute le code pendant la compilation.
- À l'aide d'un intercepteur de code au moment de l'exécution qui intercepte l'exécution et ajoute le code désiré.
Dans le .NET Framework, les plus couramment utilisés de ces techniques sont interception post-traitement et code. La première est la technique utilisée par PostSharp (postsharp.net) et ce dernier est utilisé par des conteneurs d'injection (DI) dépendance tels que Château DynamicProxy (bit.ly/JzE631) et l'unité (unity.codeplex.com). Ces outils utilisent généralement un modèle de conception nommé décorateur ou Proxy pour effectuer l'interception de code.
Le modèle de Design décorateur
Le modèle de design décorateur résout un problème commun : Vous avez une classe et à ajouter des fonctionnalités à elle. Vous disposez de plusieurs options pour cela :
- Vous pourriez ajouter la nouvelle fonctionnalité de la classe directement. Cependant, qui donne à la classe une autre responsabilité et fait mal le principe de la « seule responsabilité ».
- Vous pouvez créer une nouvelle classe qui exécute cette fonctionnalité et appeler à partir de l'ancienne classe. Cela amène un nouveau problème : Que se passe-t-il si vous souhaitez également utiliser la classe sans les nouvelles fonctionnalités ?
- Vous pouvez hériter d'une classe nouvelle et ajouter de nouvelles fonctionnalités, mais qui peut entraîner plusieurs nouvelles classes. Par exemple, supposons que vous avez une classe de référentiel pour créer, lire, mettre à jour et supprimez les opérations de base de données (CRUD) et vous souhaitez ajouter l'audit. Plus tard, vous souhaitez ajouter la validation des données pour s'assurer que les données sont mis à jour correctement. Après cela, vous pouvez également authentifier l'accès pour s'assurer que seuls les utilisateurs autorisés peuvent accéder aux classes. Voici les grandes questions : Vous pourriez avoir certaines classes qui implémentent les trois aspects, et certains qui implémentent deux d'entre eux seulement, ou même un seul. Combien de classes vous finirait par avoir ?
- Vous pouvez « décorer » la classe avec l'aspect, créez une classe qui utilise l'aspect puis appelle ancien. De cette façon, si vous avez besoin d'un seul aspect, vous décorez une fois. Pour les deux aspects, vous la décorer deux fois et ainsi de suite. Disons que vous commandez un jouet (comme nous sommes tous geeks, une Xbox ou un smartphone est OK). Il a besoin d'un paquet d'affichage dans le magasin et de protection. Ensuite, vous le commander avec la deuxième décoration, d'embellir la zone avec des bandes, des rayures, des cartes et papier cadeau d'emballage. Le magasin envoie le jouet avec un troisième paquet, une boîte avec des boules en polystyrène de protection. Vous avez trois décorations, chacune avec une fonctionnalité différente et chaque une indépendante une de l'autre. Vous pouvez acheter votre jouet avec aucun emballage cadeau, chercher à la boutique sans le boîtier externe ou même l'acheter avec aucune boîte (avec une remise spéciale!). Vous pouvez avoir votre jouet avec n'importe quelle combinaison des décorations, mais ils ne changent pas ses fonctionnalités de base.
Maintenant que vous savez sur le modèle Decorator, je montrerai comment l'implémenter en c#.
Commencez par créer une interface IRepository < T > :
public interface IRepository<T>
{
void Add(T entity);
void Delete(T entity);
void Update(T entity);
IEnumerable<T> GetAll();
T GetById(int id);
}
Mettre en œuvre avec le référentiel < T > classe, montré Figure 1.
Figure 1 le référentiel < T > Classe
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);
}
}
Utiliser le référentiel < T > classe pour ajouter, mettre à jour, supprimer et récupérer les éléments de la classe Customer :
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
Le programme pourrait ressembler à quelque chose comme Figure 2.
Figure 2 le programme principal, avec aucune journalisation
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();
}
Lorsque vous exécutez ce code, vous verrez quelque chose comme Figure 3.
Figure 3 sortie du programme avec aucun log
Imaginez que votre patron vous demande d'ajouter l'enregistrement à cette classe. Vous pouvez créer une nouvelle classe qui orneront IRepository < T >. Il reçoit la classe pour construire et implémente la même interface, comme le montre Figure 4.
La figure 4 le référentiel Logger
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;
}
}
Cette nouvelle classe encapsule les méthodes pour la classe décorée et ajoute la fonctionnalité d'enregistrement. Vous devez modifier le code pour appeler la classe de journalisation, comme le montre Figure 5.
Figure 5 le programme principal en utilisant le référentiel Logger
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();
}
Vous suffit de créer la nouvelle classe, en passant une instance de la classe vieux comme paramètre à son constructeur. Lorsque vous exécutez le programme, vous pouvez voir il a l'enregistrement, comme le montre Figure 6.
Figure 6 l'exécution du programme de consignation avec un décorateur
Vous pensez peut-être : "OK, l'idée est bonne, mais il y a beaucoup de travail : Je dois mettre en place toutes les classes et ajouter l'aspect de toutes les méthodes. Ce sera difficile à maintenir. Y a-t-il une autre façon de le faire? » Avec le .NET Framework, vous pouvez utiliser la réflexion pour obtenir toutes les méthodes et les exécuter. La bibliothèque de classe de base (BCL) a même la classe RealProxy (bit.ly/18MfxWo) qui effectue la mise en œuvre pour vous.
Création d'un Proxy dynamique avec RealProxy
La classe RealProxy vous offre les fonctionnalités de base pour les proxys. C'est une classe abstraite qui doit être héritée par la substitution de la méthode Invoke et ajout de nouvelles fonctionnalités. Cette classe est l'espace de noms System.Runtime.Remoting.Proxies. Pour créer un proxy dynamique, vous utilisez du code similaire à Figure 7.
Figure 7 la classe de Proxy dynamique
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);
}
}
}
Dans le constructeur de la classe, vous devez appeler le constructeur de la classe de base, en passant le type de la classe à décorer. Ensuite, vous devez substituer la méthode Invoke qui reçoit un paramètre IMessage. Il contient un dictionnaire avec tous les paramètres passés à la méthode. Le paramètre IMessage est catalogué à un IMethodCallMessage, donc vous pouvez extraire le paramètre MethodBase (qui a le type MethodInfo).
Les prochaines étapes consistent à ajouter l'aspect souhaité avant d'appeler la méthode, appelez la méthode methodInfo.Invoke originale, puis ajouter l'aspect après l'appel.
Vous ne pouvez pas appeler votre proxy directement, car DynamicProxy < T > n'est pas un IRepository < client >. Cela signifie que vous ne peut pas l'appeler comme ceci :
IRepository<Customer> customerRepository =
new DynamicProxy<IRepository<Customer>>(
new Repository<Customer>());
Pour utiliser le référentiel décoré, vous devez utiliser la méthode GetTransparentProxy qui retourne une instance de IRepository < client >. Chaque méthode de cette instance qui s'appelle passera par la méthode du proxy Invoke. Pour faciliter ce processus, vous pouvez créer une classe de fabrique pour créer le proxy et retourner l'instance pour le référentiel :
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>;
}
}
De cette façon, le programme principal sera semblable à Figure 8.
Figure 8 le programme principal avec un Proxy dynamique
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();
}
Lorsque vous exécutez ce programme, vous obtenez un résultat similaire comme avant, comme le montre Figure 9.
Exécution du programme figure 9 avec Proxy dynamique
Comme vous pouvez le voir, vous avez créé un proxy dynamique qui permet d'ajouter des aspects du code, avec pas besoin de le répéter. Si vous souhaitez ajouter un nouvel aspect, il vous suffit créer une nouvelle classe, héritez de RealProxy et l'utiliser pour décorer le premier proxy.
Si votre patron vous revient et vous demande d'ajouter l'autorisation pour le code, donc seuls les administrateurs peuvent accéder le référentiel, vous pouvez créer un nouveau proxy comme sur la Figure 10.
Figure 10 un Proxy d'authentification
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);
}
}
L'usine de référentiel doit être modifiée pour appeler les deux procurations, comme le montre Figure 11.
La figure 11, l'usine de référentiel décoré par deux procurations
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;
}
}
Lorsque vous modifiez le programme principal à Figure 12 et exécutez-le, vous obtiendrez le résultat illustré Figure 13.
Figure 12 le programme principal qui appelle le référentiel avec deux utilisateurs
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();
}
Figure 13 sortie du programme à l'aide de deux procurations
Le programme exécute les méthodes de dépôt deux fois. La première fois, il s'exécute en tant qu'utilisateur admin et les méthodes sont appelées. La deuxième fois, il s'exécute comme un utilisateur normal et les méthodes sont ignorés.
C'est beaucoup plus facile, n'est-il pas ? Notez que l'usine retourne une instance de IRepository < T >, donc le programme ne sait pas si elle utilise la version décorée. Cela respecte le principe de Substitution de Liskov, qui dit que si S est un sous-type de T, puis objets de type T peuvent être remplacés par des objets de type S. Dans ce cas, en utilisant un IRepository < client > interface, vous pouvez utiliser n'importe quelle classe implémentant cette interface sans changement dans le programme.
Fonctions de filtrage
Jusqu'à présent, il n'y n'avait aucun filtrage dans les fonctions ; l'aspect est appliqué à chaque méthode de classe qui est appelée. Souvent, ce n'est pas le comportement souhaité. Par exemple, vous ne pouvez pas connecter des méthodes de récupération (GetAll et GetById). C'est une façon d'y parvenir pour filtrer l'aspect par nom, comme dans Figure 14.
Figure 14 méthodes de filtrage pour l'Aspect
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);
}
}
Le programme vérifie si la méthode commence par « Get ». Dans l'affirmative, le programme ne s'applique pas l'aspect. Cela fonctionne, mais le code de filtrage est répété trois fois. En outre, le filtre est à l'intérieur du proxy, qui vous fera changer la classe chaque fois que vous souhaitez modifier le proxy. Vous pouvez améliorer cela en créant un prédicat de IsValidMethod :
private static bool IsValidMethod(MethodInfo methodInfo)
{
return !methodInfo.Name.StartsWith("Get");
}
Maintenant vous devez apporter la modification dans un seul endroit, mais vous devez encore modifier la classe chaque fois que vous souhaitez modifier le filtre. Une solution à cela serait d'exposer le filtre comme une propriété de la classe, donc vous pouvez assigner la responsabilité de la création d'un filtre à l'appelant. Vous pouvez créer un propriété de filtre de type prédicat < MethodInfo > et l'utiliser pour filtrer les données, comme le montre Figure 15.
Figure 15 filtrage Proxy
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);
}
}
}
La propriété Filter est initialisée avec filtre = m = > valeur true. Cela signifie qu'il n'y a pas de filtre actif. Lorsque vous affectez à la propriété Filter, le programme vérifie si la valeur est nulle et que, dans l'affirmative, il réinitialise le filtre. Dans l'exécution de la méthode Invoke, le programme vérifie le résultat du filtre et, si elle est vraie, elle s'applique à l'aspect. La création de proxy dans la classe de fabrique ressemble maintenant à ceci :
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>;
}
}
}
La responsabilité de créer le filtre a été transférée à l'usine. Lorsque vous exécutez le programme, vous devriez obtenir quelque chose comme Figure 16.
Figure 16 sortie avec un Proxy filtré
Remarquez dans Figure 16 qui les deux dernières méthodes, GetAll et GetById (représentée par la « Mise en entités » et "Getting entité 1 ») n'est pas journalisation autour d'eux. Vous pouvez améliorer la classe encore plus loin en exposant les aspects en tant qu'événements. De cette façon, vous n'avez pas à modifier la classe chaque fois que vous souhaitez modifier l'aspect. Ceci est illustré dans Figure 17.
Figure 17 Proxy Flexible
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);
}
}
}
Dans Figure 17, trois événements, BeforeExecute, AfterExecute et ErrorExecuting, sont appelées par les méthodes OnBeforeExecute, OnAfterExecute et OnErrorExecuting. Ces méthodes vérifier si le gestionnaire d'événements est défini et, si c'est le cas, ils vérifient si la méthode appelée passe le filtre. Dans l'affirmative, qu'ils appellent le gestionnaire d'événements qui s'applique à l'aspect. La classe de fabrique maintenant devient quelque chose comme Figure 18.
Figure 18 A Repository Factory qui définit les événements de l'Aspect et le filtre
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>;
}
}
Maintenant vous avez une classe proxy flexible et vous pouvez choisir les éléments à appliquer avant l'exécution, après l'exécution, ou lorsqu'il y a une erreur et que pour les méthodes sélectionnées. Avec cela, vous pouvez appliquer plusieurs aspects dans votre code avec pas de répétition, d'une manière simple.
Pas un remplacement
Avec AOP vous pouvez ajouter du code pour toutes les couches de votre application de manière centralisée, sans avoir besoin de répéter le code. J'ai montré comment créer un proxy dynamique générique basé sur le modèle de design décorateur s'appliquant des aspects à vos classes à l'aide d'événements et un prédicat pour filtrer les fonctions souhaitées.
Comme vous pouvez le constater, la classe RealProxy est une classe flexible et vous donne le plein contrôle du code, sans dépendances externes. Toutefois, notez que RealProxy n'est pas un remplacement pour les autres outils de l'AOP, comme PostSharp. PostSharp utilise une méthode complètement différente. Il ajoutera le code IL (intermediate language) dans une étape de post-compilation et ne sera pas utiliser la réflexion, elle devrait avoir plus performant que le RealProxy. Vous aurez aussi à faire plus de travail pour mettre en œuvre un aspect avec RealProxy qu'avec PostSharp. Avec PostSharp, vous devez uniquement créer la classe d'aspect et ajouter un attribut à la classe (ou la méthode) où vous voulez l'aspect ajouté, et c'est tout.
En revanche, avec RealProxy, vous aurez le contrôle total de votre code source, sans dépendances extérieures, et vous pouvez étendre et personnaliser autant que vous le souhaitez. Par exemple, si vous souhaitez appliquer un aspect uniquement sur les méthodes qui ont l'attribut Log, vous pourriez faire quelque chose comme ceci :
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);
}
...
En outre, la technique utilisée par RealProxy (intercepter le code et permettre au programme de le remplacer) est puissant. Par exemple, si vous souhaitez créer une infrastructure Mocking, pour créer des simulacres génériques et les stubs pour tester, vous pouvez utiliser la classe RealProxy pour intercepter tous les appels et les remplacer par votre propre comportement, mais c'est un sujet pour un autre article !
Bruno Sonnino est un Microsoft Most Valuable Professional (MVP) situé au Brésil. Il est un développeur, consultant et auteur, ayant écrit cinq livres de Delphi, publiés en portugais par Pearson Education Brésil et de nombreux articles pour le Brésil et les États-Unis magazines et sites Web.
Remercie les experts techniques Microsoft Research suivants d'avoir relu cet article : James McCaffrey, Carlos Suarez et Johan Verwey
James McCaffrey travaille pour Microsoft Research à Redmond, Washington Il a travaillé sur plusieurs produits Microsoft, y compris Internet Explorer et Bing. Il peut être contacté à jammc@microsoft.com.
Carlos Garcia Jurado Suarez est ingénieur logiciel de recherche de Microsoft Research, où il a travaillé dans l'équipe de développement avancé et plus récemment le groupe de réflexion de la Machine. Auparavant, il était un développeur en Visual Studio travaille sur la modélisation des outils tels que le concepteur de classes.