Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Programmation par Aspect en C# avec RealProxy

[new:30/09/2015]Une bonne programmation est découplée mais de nombreux besoins comme les logs par exemple peuvent traverser les couches et n’avoir aucun lien hiérarchique avec elles. La programmation par aspect est une réponse. Et C# offre des solutions élégantes…

AOP – Aspect Oriented Programming

Je citerai juste pour rappel Wikipédia :

“l’AOP est un paradigme de programmation qui permet de traiter séparément les préoccupations transversales (cross-cutting concerns) qui relèvent souvent de la technique (aspect), des préoccupations métier qui constituent le cœur d’une application.” (ref: wikipédia)

On le comprend bien, l’organisation d’un logiciel bien fait se veut fortement découplée et souvent organisée selon le métier et des couches spécialisées (type DAL, BOL…). Cela fait déjà plusieurs axes de découpage plusieurs dimensions d’un espace virtuel à prendre en compte et c’est en cela que créer des architectures solides est un vrai métier. Comme souvent lorsqu’un logiciel est bien conçu et qu’on en regarde le code on peut … ne rien se dire. Ca semble propre, bien séparé, finalement c’est “naturel”, “facile”… Mais tout le monde sait que donner l’impression de la facilité et de l’aisance est ce qui réclame souvent le plus de réflexion et de savoir-faire !

J’aime dans un tel cas cité mon ami Jean-Sébastien qui disait malicieusement “L’orgue c’est facile, il suffit d’appuyer sur la bonne touche au bon moment”. Quelle farceur ce JS ! Tout est dans dans la “bonne” touche et le “bon” moment. Mais sinon c’est facile. Certains morceaux de Bach peuvent se jouer avec deux doigts et vous faire voyager très loin. La bonne touche au bon moment…

L’architecture d’un logiciel c’est la même chose, la bonne décision, le bon découplage, au bon endroit. Et quand cela coule de source (!) et que c’est bien réalisé, l’œil du développeur standard ne verra rien de spécial. Il aura même l’impression fallacieuse que c’est un code qu’il aurait pu écrire lui-même… En revanche celui qui sait comprend la difficulté de la démarche.

De fait bien architecturer un code en respectant la logique métier, les séparations techniques, les grands “faut pas!” comme KISS, DRY et YAGNI est déjà une affaire complexe (voir la About page de E-naxos pour la définition de ces sigles !). Mais même lorsque tout est bien géré de la sorte il reste des besoins qui dépassent ces découpages et découplages. C’est cela que la définition de Wikipédia nous indique en parlant de “préoccupations transversales”.

Les besoins qui sortent du cadre

Il n’est pas rare qu’en plein milieu d’un développement on ait à faire face à certains besoins qui ne peuvent pas se régler facilement par la création d’une couche supplémentaire, d’un service, d’une simple injection de dépendances. Supposons par exemple :

  • Que les données envoyées à la base SQL doivent être pré-validées de façon globale (test de cohérence ou autre)
  • Qu’on doivent ajouter de quoi auditer le code avec plus de finesse dans certaines opérations
  • Qu’il faille s’assurer de l’authentification de l’utilisateur avant chaque requête aux données (en lecture et en écriture par exemple)
  • Qu’il faille mesurer avec finesse le temps de certains traitements pour les monitorer et vérifier qu’ils restent dans des bornes fixées
  • Ou tout simplement qu’il faille ajouter un système de log très invasif pour pister toutes les opérations en débogue… Etc…

 

Chacune des ces nouvelles obligations peuvent entrainer un énorme travail avec à la clé l’une des pires horreurs, la répétition qui viole le saint DRY et rendra la maintenance particulièrement “rock’n’roll” donc couteuse…

Dans de tels cas on aimerait disposer d’un pré-processeur ou d’un autre moyen technique pour que le compilateur lui-même soit capable d’ajouter partout le code nécessaire.

Si C# ne propose pas de solutions de ce type, nous allons voir qu’il est possible de faire des miracles avec le Framework .NET et la classe RealProxy généralement fort peu connue…

La force de l’AOP

La programmation par Aspect offre de nombreux avantages mais le plus important est certainement de permettre la centralisation en un seul point de certaines préoccupations transversales. Et ces dernières sont plus nombreuses qu’on ne le pense !

  • Gérer une authentification avant chaque opération (de type filtrage pour certains utilisateurs par exemple)
  • Ajouter un logging de debogue très fin (donc très répétitif en termes de code)
  • Instrumentaliser le code pour surveiller ses performances
  • Ajouter des évènements, des notifications ou des messages MVVM sur le changement de certaines valeurs
  • Changer le comportement de certaines méthodes, etc.

 

L’AOP a aussi ses dangers : du code est exécuté en dehors des méthodes lorsque ces dernières sont appelées. Il y a quelque chose d’invisible, de magique qui se passe et qui peut rendre la compréhension de certains comportement voire bogue bien plus difficile. Un bogue dans la partie AOP se répètera partout et aura un impact énorme et sera délicat à pister. L’AOP a donc un cout et ce dernier doit être évaluer en fonction du gain espéré. Si on n’utilise que quelques fois l’AOP dans un logiciel mieux vaut encore répéter certains bouts de code, cela sera plus maintenable.

Comme toutes les bonnes idées architecturales ou méthodologiques s’il peut s’agir de véritables paradigmes il ne s’agit jamais de dogmes et chaque architecte doit être en mesure, par son savoir-faire et sa compétence, de faire les meilleurs choix dans chaque cas particulier. Et chaque application est un cas particulier.

Il existe de nombreuses façons d’implémenter l’AOP comme par exemple l’utilisation d’un pré-processeur comme je l’évoquais plus haut ou un post-processeur qui ajoute du code binaire (ou IL) à l’exécutable. On peut aussi utiliser certains compilateurs capables d’ajouter eux-mêmes le code à la compilation ou utiliser un intercepteur de code au runtime qui intercepte les instructions visées et ajoute à ce moment le comportement désiré.

Sous .NET on utilise plus généralement deux techniques : le post-processing et l’interception de code.

On retrouve la première de ces techniques dans PostSharp (qui existe en licence free limitée et en version payante couvrant plus de besoins) et la seconde dans les conteneur d’injection de dépendance comme Castle DynamicProxy et Unity.

Je ne reparlerai pas ici de ce qu’est l’injection de dépendance de très nombreux articles de Dot.Blog abordent déjà la question. Quant au post-processing tout le monde voit de quoi il s’agit je pense.

Ce qui mérite d’être noté c’est qu’en général les outils proposés reposent sur l’application d’un design pattern nommé Décorateur ou Proxy pour effectuer l’interception du code.

Les Design Pattern Décorateur et Proxy

Il ne faut trop pinailler mais ces deux patterns ne sont pas tout à fait identiques. J’aime m’en tenir à la bible en la matière, le livre de Gamma, Helm, Johnson et Vlissides, autrement appelés “le gang des quatre”.

Décorateur et proxy sont deux patterns du groupe dit “structurel”.
Les patterns de cette catégorie se préoccupent de la façon dans les classes et les objets sont composés pour former des structures plus grandes. On retrouve, outre les deux déjà citées, l’Adaptateur, le Pont, le Composite, la Façade et le Flyweight (“poids-mouche”).

La grande différence entre Décorateur et Proxy se tient dans le fait que le Décorateur se focalise sur l’ajout dynamique de fonctions à un objet alors que le Proxy s’intéresse plus au contrôle de l’objet lui-même. Le Proxy est généralement un procédé compile-time (le proxy est compilé comme le reste du code et c’est lui qui se débrouille pour instancier l’objet dont il est le proxy) alors que le Décorateur est plutôt un procédé runtime, il est assigné à un objet existant durant l’exécution de l’application. On verra plus loin, la nuance entre décorateur et proxy devient encore plus équivoque si on utilise un proxy dynamique (donc runtime).

Mais il s’agit ici de généralités qui peuvent ou non se retrouver dans toutes les implémentations et utilisations possibles de ces deux patterns.

Selon le “gang of four” l’intention du Décorateur est d’attacher des responsabilités additionnelles à un objet de façon dynamique. Il est présenté comme une alternative flexible au sous-classement pour étendre une classe. La définition du Proxy est assez éloignée de prime abord puisqu’il s’agit de fournir une substitution, un remplaçant pour un autre objet pour le contrôler et y accéder. Nous verrons dans les faits que les proxy dynamiques et les décorateurs peuvent dans certains cas se confondre, voire se fondre en un troisième concept qui n’a pas vraiment de nom.

Le décorateur à la structure suivante :

decor064

Les participants sont :

Component : définit l’interface des objets qui peuvent avoir des responsabilités ajoutées dynamiquement

  • ConcreteComponent :  Définit un objet auquel des responsabilités sont ajoutées dynamiquement
  • Decorator : Il maintient une référence sur un objet de type Component et définit une interface qui se conforme à celle de Component
  • ConcreteDecorator : Les différents décorateurs réels qui ajoutent des responsabilités au composant.

 

Pourquoi utiliser le Décorateur ?

Pour rester concrets essayons de voir plutôt des cas dans lesquels nous ne l’utiliserions pas et quelles conséquences cela pourrait avoir :

  • On peut par exemple ajouter la nouvelle fonctionnalité directement dans la ou les classes concernées. Mais cela donne à ces dernières de nouvelles responsabilités ce qui en change le sens et va à l’encontre du principe “une classe = une responsabilité”.

  • On peut aussi créer une nouvelle classe qui exécute la nouvelle fonctionnalité et l’appeler depuis l’ancienne classe. C’est un peu tordu mais surtout que ce passe-t-il si on souhaite utiliser la classe sans la nouvelle fonctionnalité ?

  • On peut aussi hériter de la classe à enrichir et ajouter la nouvelle fonctionnalité dans la sous-classe. Mais ici on va multiplier les classes ce qui n’est pas non plus souhaitable… Imaginons que nous ayons une classe qui réalise les opérations CRUD sur une table. Si on veut ajouter du code d’audit on peut en effet la sous-classer et instrumentaliser les méthodes. D’une part il faut que la classe ne soit pas SEALED et que ces méthodes soient virtuelles ce qui pose une contrainte forte à cette méthode. Mais admettons. Plus tard on veut ajouter un mécanisme de validation des données. On sous-classe la sous-classe… Et puis un nouveau besoin d’authentification s’exprime et il faudra sous-classer la sous-classe de la sous-classe… Aie aie aie … Je ne parle même pas du choix dans l’utilisation de toutes ces classes, des morceaux de code anciens qui auront des opérations CRUD instrumentalisées mais non authentifiées et toute sorte de mélanges de ce genre. Une horreur à fuir !

  • Enfin on peut décorer la classe avec le nouvel aspect. C’est-à-dire créer une nouvelle classe qui utilise l’aspect et qui appelle l’ancienne classe pour tout le reste. De cette façon si on veut ajouter un nouvel aspect on décore la classe une fois. Si on veut ajouter deux nouveaux aspects on décore la classe deux fois, etc. Pour mieux comprendre imaginons un smartphone. Il a besoin d’un emballage de présentation en magasin. On peut aussi vouloir l’offrir dans ce cas il sera emballé dans du papier cadeau avec un petit nœud. On peut vouloir le faire livrer, il sera alors emballé dans un carton avec du papier bulle, le fabriquant l’emballe dans des cartons, eux-mêmes dans des palettes, des containers pour bateau, etc. Chaque emballage s’ajoute au précédent mais peu très bien “fonctionner” seul. On peut expédier un smartphone sans emballage cadeau par exemple ou utiliser un container pour expédier une voiture. Et tous ces emballages (“décorateurs”) sont indépendants les uns des autres mais aussi et surtout le sont vis-à-vis du smartphone qui peut même fonctionner sans emballage du tout… Si vous comprenez cette métaphore vous venez de comprendre le principe du Décorateur !

Implémenter le décorateur

Repartons de notre idée d’opérations CRUD utilisée pour illustrer le propos en début d’article.  Je m’inspire ici du code publié dans un numéro de 2014 de MSDN Magazine qui m’avait séduit et dont je m’étais juré de vous parler, mieux vaut tard que jamais !

On peut définir une interface générique pour toutes les entités qui supportent ces opérations, cela donnera :

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

 

Implémentons maintenant une classe qui support les opérations CRUD de IRepository<T> :

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);
  }
}

 

Bien entendu il s’agit d’une classe qui simule les opérations, rien ne sert de compliquer le code exemple par des accès réels à une base de données. Le repository concret pourrait utiliser le nom de la classe T pour déduire le nom de la table concernée par exemple, ou un service ou une interface supportée par les entités qui retourne ce nom de table etc. De même on laisse de côté ici la logique des transactions et autres mécanisme de connexion.

On peut dès lors utiliser notre repository concret sur la classe Customer que nous définissons de la sorte :

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

 

On remarque que cette classe est totalement ignorante des opérations CRUD.

Mais grâce au mécanisme utilisé on peut persister ou manipuler les objets de type Customer de la façon suivante (j’ai utilisé LinqPad pour tester le code, un peu de pub au passage pour cet outil génial !) :

  Console.WriteLine("***Début de Programme***");
  var 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\nFin du programme***");

 

Un run donnera la sortie suivante :

***Début de Programme***
Adding UserQuery+Customer
Updating UserQuery+Customer
Deleting UserQuery+Customer

Fin du programme***

 

Décorer  pour simplifier

C’est ici que les choses deviennent intéressantes.

Vous avez créé la classe Customer. On vous a demandé qu’elle supporte des opérations CRUD mais au lieu de toucher à la classe elle-même vous avez choisi de créer IRepository<T> et sa classe concrète. C’était une bonne idée, la classe Customer n’a pas à se mélanger les pinceaux dans des problèmes de persistances sur une base SQL. Ce n’est pas sa responsabilité. Customer garde tout son sens et peut être utilisé dans mille contextes même ceux n’impliquant aucune base de données… Quel choix d’architecture judicieux ! On ajoutera que dans la réalité l’implémentation de IRepository<T> reposera certainement sur des classes écrites dans le DAL ou sur une couche ORM de type Entity Framework.

Mais voici que votre chef vient vous dire “au fait machin, j’ai oublié de te dire qu’il nous faut un log de toutes les opérations pour le débogue”.

Rien de plus simple ! Au lieu de bricoler encore et encore cette pauvre classe Customer qui serait désormais méconnaissable et qui aurait perdu toute notion de sa responsabilité originelle il suffit créer un Décorateur !

Créons une classe LoggerRepository<T> qui implémentera IRepository<T> et qui pourra décorer directement une classe Repository<T> (la classe Customer est toujours totalement absente de ces manipulations vous noterez) :

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;
  }
}

 

Rien de compliqué, LoggerRepository<T> ne fait qu’implémenter l’interface déjà définie plus haut. Seulement elle possède un constructeur auquel on passe un objet supportant IRepository<T>, objet dont les méthodes sont désormais précédées et suivies d’un log...

Comment utiliser cette nouvelle classe ? En modifiant juste une ligne de notre programme :

  Console.WriteLine("***Début de programme***");
  // var customerRepository = new Repository<Customer>();
  var 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\nFin du programme***");

 

Vous remarquez que lors de la création de l’objet repository nous utilisons désormais un LoggerRepository<Customer> auquel nous passons en référence une instance de Repository<Customer> …

Du coup, sans rien toucher la sortie du programme devient :

***Début de programme***
In decorator - Before Adding UserQuery+Customer
Adding UserQuery+Customer
In decorator - After Adding UserQuery+Customer
In decorator - Before Updating UserQuery+Customer
Updating UserQuery+Customer
In decorator - After Updating UserQuery+Customer
In decorator - Before Deleting UserQuery+Customer
Deleting UserQuery+Customer
In decorator - After Deleting UserQuery+Customer

Fin du programme***

 

C’est génial mais…

Je vous vois venir…

Je vous connais… Toujours un peu tatillon sur les bords. “Oui, tout ça c’est très beau, encore une belle démo d’un beau principe de chercheur en blouse blanche, mais moi j’ai plein de classes et je ne vais pas réimplementer toutes les méthodes et ajouter tous les aspects et tout ce boulot de fou !

Et je vous entends bien. N’ayez crainte. Cela serait beaucoup de travail et compliquerait la maintenance ce qu’on cherche à éviter justement…

En fait sous .NET et grâce à cette géniale idée qu’est la réflexion qui permet à un programme de se connaitre lui-même il serait tout à fait possible d’automatiser le Décorateur et d’appliquer les logs de notre exemple à toutes les méthodes de n’importe quel objet.

C’est encore trop de travail ?

J’ai ce qu’il vous faut !

RealProxy

Le framework .NET est une merveille. Cette API objectivée a été conçue avec un tel soin et par des gens si compétents, des méthodologistes et des ingénieurs qui méritent mille fois ce titre, que même ce genre de besoin est couvert ! Tout à l’intérieur du Framework, mais accessible à l’extérieur. A l’intérieur car les concepteurs de .NET au fait des bonnes pratiques ont utilisé le mécanisme pour leur propre code… A l’extérieur car ils ont été assez compétents pour développer quelque chose qui était exposable, pas un bricolage pour soi-même qu’on cache dans le binaire d’une sombre DLL…

Cette classe s’appelle RealProxy, présente depuis la version 1.1 au moins de .NET, c’est dire si elle est à la base même de tout l’édifice et si ce dernier a été bien conçu dès le départ.

Le principe de RealProxy est de fournir les mécanismes de base pour réaliser ce que nous disions plus haut, décorer une classe en utilisant la réflexion. RealProxy est une classe abstraite il faut donc en créer des émanations concrètes, ce qui est logique puisque .NET ne peut pas deviner toutes les décorations possibles. .NET c’est fantastique mais pas magique.

Une fois qu’on a hérité de RealProxy il suffit de surcharger sa méthode Invoke pour ajouter de nouvelles fonctionnalités à la classe décorée (aux méthodes appelées dans cette classe). On trouve cette petite perle bien cachée dans son coquillage au fin fond de System.Runtime.Remoting.Proxies.

Oui, le namespace contient “Remoting”. Vous rappelez-vous  de .NET Remoting ? c’était avant WCF. Et cela permettait déjà de travailler sur des instances distantes… Et forcément il fallait un proxy pour décorer les classes de l’utilisateur afin de déporter les appels aux méthodes. Objet proxy en local, objet réel sur le serveur, et RealProxy pour jouer le rôle de décorateur…

Implémentation

Reprenons l’idée de notre décorateur qui ajoute des logs à chaque méthode du Repository.

Avec RealProxy nous écrirons :

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 et passer le type de la classe décorée. Vous surcharger ensuite Invoke qui reçoit un IMessage en paramètre. Ce paramètre contient un dictionnaire de tous les paramètres passés à la méthode appelée. On le transtype en IMethodCallMessage afin d’extraire le paramètre MethodBase (de type MethodInfo).

Ne reste plus qu’à ajouter les aspects désirés et à appeler la méthode originale grâce à MethodInfo.Invoke. On ajoute un second aspect après l’appel pour terminer (dans le cas présent on souhaite avoir un log avant et après l’appel de chaque méthode, ce n’est qu’un exemple).

Avec la logique en place on aimerait appeler directement notre DynamicProxy<T> mais ce n’est pas un IRepository<T>, de fait on ne peut pas écrire :

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

 

Pour utiliser le proxy dynamique on doit utiliser sa méthode GetTransparentProxy qui retourne une instance de IRepository<Customer> dans notre cas. Chaque méthode de cette instance particulière qui sera appelée passera par la méthode Invoke du proxy. Pour simplifier tout cela il suffit de créer une factory qui cache cette astuce :

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>;
  }
}

 

On notera que IMessage utilisé dans le code du proxy provient du namespace System.Runtime.Remoting.Messaging. De même que ReturnMessage.

Armez de notre factory, le code de notre application se modifie de façon très simple :

Console.WriteLine("***Avec proxy dynamique***");
  // 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("***Fin***");

 

On remarque en commentaire le premier essai (création d’un Repository<T> simple), puis du second essai (avec LoggerRepository<T>) puis le nouveau code de création du repository utilisant DynamicRepository qui se base sur le code de RealProxy de .NET.

Le reste n’a pas changé d’une virgule (même si mes copies d’écran ne sont pas tout à fait identiques au fil de mes bricolages pour l’article !), la classe Customer est restée ce qu’elle est depuis le début, sa responsabilité est toujours la même, son code est clair, sa taille est très modeste (comme doit l’être toute classe). Mais Customer donne l’impression de supporter des opérations CRUD qui elles-mêmes donnent l’impression de supporter un système de log. Mais tout cela est indépendant, ne brouille pas la logique des couches en place ni ne change les classes de sens. Mieux, tout peut être utilisé seul et dans d’autres contextes. Demain on veut utiliser Customer pour faire des listes mémoire sans embarquer les namespaces propres à SQL Server, c’est possible. On souhaite avoir un repository adapté à Oracle, c’est possible. On ne veut plus des logs ? C’est possible. Bref, on a fabriqué un édifice où tout se tient mais démontable et utilisable d’une autre façon comme les emballages de ma métaphore un peu plus haut.

Au fait, la sortie du programme modifié devient :

***Avec proxy dynamique***
In Dynamic Proxy - Before executing 'Add'
Adding UserQuery+Customer
In Dynamic Proxy - After executing 'Add' 
In Dynamic Proxy - Before executing 'Update'
Updating UserQuery+Customer
In Dynamic Proxy - After executing 'Update' 
In Dynamic Proxy - Before executing 'Delete'
Deleting UserQuery+Customer
In Dynamic Proxy - After executing 'Delete' 
***Fin***

 

On a bien créé un proxy dynamique qui autorise l’ajout d’aspects au code sans avoir besoin de le répéter. Si on désire ajouter un nouvel aspect on créera seulement une nouvelle classe qui héritera de RealProxy et qu’on utilisera pour décorer le premier proxy ! On peut à l’infini décorer les décorateurs sans répéter de code ni défigurer les classes décorées.

Une nouvelle demande…

Votre chef revient… il jette un œil désabusé à votre code (ça fait longtemps qu’il ne code plus et n’y comprend rien de toute façon, mais ça fait classe de regarder avec l’air d’un connaisseur) puis vous dit “au fait coco, je t’avais dit que seuls les admins ont accès au repository. Ah bon je te l’ai pas dit ? Bon c’est pas grave, la recette avec le client c’est dans 2h au fait.

Ceux qui n’auront pas suivi cet article auront toutes les raisons de paniquer. Mais pas vous !

En deux minutes voici le code d’un nouveau proxy dynamique qui refusera obstinément tout accès au repository aux users qui ne sont pas des admins :

class AuthenticationProxy<T> : System.Runtime.Remoting.Proxies.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 System.Runtime.Remoting.Messaging.IMessage Invoke(System.Runtime.Remoting.Messaging.IMessage msg)
  {
    var methodCall = msg as System.Runtime.Remoting.Messaging.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 System.Runtime.Remoting.Messaging.ReturnMessage(result, null, 0,
          methodCall.LogicalCallContext, methodCall);
      }
      catch (Exception e)
      {
        Log(string.Format(
          "User authenticated - Exception {0} executing '{1}'", e),
          methodCall.MethodName);
        return new System.Runtime.Remoting.Messaging.ReturnMessage(e, methodCall);
      }
    }
    Log("User not authenticated - You can't execute '{0}' ",
      methodCall.MethodName);
    return new System.Runtime.Remoting.Messaging.ReturnMessage(null, null, 0,
      methodCall.LogicalCallContext, methodCall);
  }
}

 

Dernière chose : modifier la Factory pour que les deux proxies soient appelés (l’un décorant l’autre n’oubliez pas ce jeu de poupées russes !) :

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;
  }
}

 

Sans changer une ligne à l’application voici sa sortie :

***Avec proxy dynamique***
User not authenticated - You can't execute 'Add' 
User not authenticated - You can't execute 'Update' 
User not authenticated - You can't execute 'Delete' 
***Fin***

 

Notre code est bien sécurisé, pas de user identifié, pas d’accès aux méthodes du repository…

Mais modifions maintenant le code de création du repository dans notre application pour fournir un user ayant un rôle d’administrateur puis un user sans rôle d’admin :

Console.WriteLine(
    "***Début avec authentification***");
  Console.WriteLine("\r\nROLE ADMIN");
  Thread.CurrentPrincipal =
    new System.Security.Principal.GenericPrincipal(new System.Security.Principal.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\nROLE SIMPLE USER");
  Thread.CurrentPrincipal =
    new System.Security.Principal.GenericPrincipal(new System.Security.Principal.GenericIdentity("NormalUser"),
    new string[] { });
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine(
    "***Fin***");
    Console.ReadLine();

 

Et voyons tout de suite une sortie de ce code :

***Début avec authentification***

ROLE ADMIN
User authenticated - You can execute 'Add' 
In Dynamic Proxy - Before executing 'Add'
Adding UserQuery+Customer
In Dynamic Proxy - After executing 'Add' 
User authenticated - You can execute 'Update' 
In Dynamic Proxy - Before executing 'Update'
Updating UserQuery+Customer
In Dynamic Proxy - After executing 'Update' 
User authenticated - You can execute 'Delete' 
In Dynamic Proxy - Before executing 'Delete'
Deleting UserQuery+Customer
In Dynamic Proxy - After executing 'Delete' 

ROLE SIMPLE USER
User not authenticated - You can't execute 'Add' 
User not authenticated - You can't execute 'Update' 
User not authenticated - You can't execute 'Delete' 
***Fin***

 

N’est-ce pas un peu magique ? Si forcément. L’architecture et la méthodologie sont des sciences qui ne se voient pas, on ne peut voir que du code qui met ou non en œuvre les principes qu’elles préconisent… Ce n’est que pur concept, impalpable, accessible uniquement à l’intelligence de celui qui regarde le code…

On notera que la Factory retourne toujours un IRepository<T> c’est ce qui fait que le programme fonctionne normalement même sans modification comme nous l’avons vu juste avant de l’adapter aux deux types d’utilisateur. Cela respecte le Principe de Substitution de Liskov qui pose que si S est un sous-type de T alors les objets de type T peuvent être remplacés par des objets de type S.

Dans notre cas en utilisant l’interface IRespository<Customer> on peut utiliser n’importe quelle classe qui l’implémente sans aucune modification dans le programme …

Filtrer le proxy

Il est vrai que jusqu’ici nous n’avons fait que des choses systématiques s’appliquant à toutes les méthodes d’une classe. C’est en réalité une situation assez rare. Par exemple pour notre système de log nous pouvons ne pas souhaiter monitorer les méthodes Get mais uniquement les méthodes agissant sur les données. Un des moyens simples d’y arriver est d’effectuer un filtrage sur le nom des méthodes. On remarque d’ailleurs que les bonnes pratiques de nommage rendent les choses faciles (consistance principalement, pas d’abréviation, etc). Si nous avons un GetById(int id) dans une classe un ReturnById(int id) dans une autre il sera difficile d’écrire un code générique pour le filtre…

Un tel filtrage pourrait s’écrire de la sorte (détail de la méthode invoke du proxy) :

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 nom des méthodes est ici testé sur le préfixe “Get”. Si ce préfixe est présent l’aspect n’est pas ajouté sinon il l’est.

Il y a quelques défauts à cette implémentation, d’abord DRY… le même test est répété trois fois ce qui est beaucoup en si peu de lignes ! Mais plus gênant conceptuellement, le filtre est défini dans le Proxy ce qui rendra toute modification pénible. Les choses peuvent être grandement améliorées en adoptant un prédicat IsValidMethod comme le montre ce code :

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

 

Les changements sont maintenant centralisés en un seul point. DRY est respecté. Mais il faut toujours modifier le Proxy pour modifier le filtre.

Pour régler ce dernier problème il faut complexifier un peu le code du Proxy pour lui ajouter une propriété de type Predicate<MethodInfo> et l’utiliser ensuite comme filtre. Cela donne le code suivant :

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é est initialisée avec un Filter = m => true; ce qui signifie que toutes les méthodes passent le test, il n’y a aucun filtrage.

Pour revenir sur le même filtrage des “Get” que précédemment il faut désormais passer le filtre lors de la création du Proxy ce que nous pouvons faire dans la Factory pour que tout reste centralisé et transparent pour l’application :

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>;
    }  
  }
}
 

Un cran plus loin

On peut souhaiter aller encore plus loin et il faut le faire… Plus le code sera générique plus il sera facilement réutilisable.

Ici notre décorateur est spécialisé dans l’ajout de logs autour des méthodes. Même avec l’astuce du filtre sous forme de prédicat cela reste toujours un ajout de log autour des méthodes.

On peut vouloir généraliser le proxy en transformant les logs en évènements. De cette façon il sera très facile de modifier l’aspect, qu’on ajoute des logs, ou qu’on fasse n’importe quoi d’autre.

Certes le code devient un peu plus long, mais le proxy devient immuable, il peut servir sans modification à toute sorte d’aspects :

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);
    }
  }
}

 

Trois évènements ont été créés : BeforeExecute, AfterExecute et ErrorExecuting. Ces méthodes sont appelées par les méthodes “Onxxxx” correspondantes qui vérifie si un gestionnaire d’évènement a été défini ou non et dans l’affirmative si la méthode passe le filtrage. Seulement dans ces cas précis l’évènement est appelé et l’aspect peut être ajouté.

La Factory devient alors :

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>;
  }
}

 

On dispose maintenant d’une classe Proxy qui peut s’adapter à toute sorte d’aspects de façon fluide et transparente et qui sait même filtrer les méthodes via un prédicat totalement libre. Beaucoup de choses peuvent désormais se régler sans aucune répétition et sans casser les responsabilités des classes de vos applications !

Pas une panacée, mais un code puissant

L’intérêt du code ici présenté est qu’il ne dépend de rien d’autre que de lui même et qu’il est facilement personnalisable (en dehors d’être formateur lorsqu’on essaye de le comprendre et de le mettre en œuvre, aspect pédagogique plus attrayant pour les colonnes de Dot.Blog que de vous dire d’utiliser un produit tout fait…).

Dans certains cas cela peut s’avérer suffisant et même nécessaire. Il est agréable d’avoir la totale maitrise de son code source et de ne dépendre d’aucun code externe dont les évolutions restent toujours une question sans réponse absolue.

Toutefois ne nous y méprenons pas, un outil comme PostSharp est autrement plus subtile par exemple. Il agit au niveau du code IL et n’utilise pas la réflexion. Si les performances comptent alors c’est une meilleure solution !

On peut encore faire évoluer le code étudié ici par exemple en ajoutant le support des attributs pour choisir l’aspect à appliquer. On gagne forcément en souplesse d’utilisation au prix d’une petite sophistication du proxy, la méthode Invoke pouvant devenir quelque chose comme :

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);
  }
    ...

 

Conclusion

L’AOP est une approche intelligente qui minimise le code écrit et qui permet à chaque classe de conserver sa responsabilité propre sans altération. Sa façon d’agir transversalement vient compléter sans abimer les concepts habituels de séparation des couches.

On peut s’en servir de mille façons, tests, mocks, filtrage, log, etc.

Chaque projet pose ses problèmes particuliers, l’AOP permet de donner des réponses simples à des problèmes d’architecture parfois délicats.

Pensez-y et …

Stay Tuned !

Faites des heureux, partagez l'article !
blog comments powered by Disqus