PetShop Orienté Aspect par Thomas Gil (thomas.gil@dotnetguru.org)

Table des matières

1. Introduction

Voici plusieurs années maintenant que nous entendons parler de Programmation et de Conception Orientée Aspects. Certains chanceux ont même mis cette technologie et ses outils en pratique sur leurs propres projets, généralement en langage Java ou C. Malheureusement, en .NET, les tisseurs d'Aspects mâtures brillent... par leur absence. Peu d'entre nous ont eu l'occasion ou l'audace de s'appuyer sur Aspect#, AspectC#, Loom.net ou autre (feu) Claw.

Dans cet article, nous profitons des avancées récentes du projet AspectDNG pour vous donner un exemple concret d'architecture multi-couches .NET "tissée", et surtout pour évaluer les répercussions occasionnées par l'utilisation de l'AOP par rapport à un développement "artisanal".

L'application choisie à titre d'exemple est, pour la mille et unième fois, le PetShop: un mini-site d'achat d'animaux domestiques. Cette application est simpliste, nous en convenons volontiers, mais elle nous suffira à démontrer les principales facettes de l'AOP en général, et d'AspectDNG en particulier. Nous l'appellerons le PetShopAOP.

2. Les principes

2.1. Un soupçon d'architecture et de Conception

Côté architecture, le PetShopAOP est similaire aux versions précédentes (PetShopDNG 1.0 et 2.0 que vous retrouvez sur www.dotnetguru.org sous la rubrique "PetShopDNG"):

Récapitulons cette architecture technique sous forme d'un petit schéma:

Figure 1. Architecture technique

Côté conception détaillée, nous avons pris le parti d'aller au plus simple et surtout au plus facile à maintenir. Par exemple, les objets du domaine sont de simples classes, et ne "se cachent plus" derrière des interfaces dont l'intérêt potentiel n'est pas directement perceptible dans nos cas d'utilisations. Moins prosaïquement, nous avons été fainéants et n'avons utilisé les techniques habituelles de découplage qu'en cas de besoin immédiat et évident. Exit donc certains Design Patterns qui permettraient peut-être, plus tard, dans un autre contexte... Attendons de voir cette situation apparaître pour traiter les variations (sans forcément modifier le code actuel, comme vous pourrez le constater).

Une seule règle de découplage nous a semblée essentielle: l'indépendance absolue entre les couches service et domine d'un côté, et l'outil de mapping Objet/Relationnel de l'autre. Un niveau intermédiaire permet ce découplage: la couche d'accès aux données constituée par quelques services techniques utilitaires pour faciliter l'interaction avec NHibernate ainsi qu'un DAO (Data Access Object) pour chaque type d'objet persistant. A nouveau, un diagramme résumera parfaitement nos choix de conception:

Figure 2. Package diagram

Enfin, évoquons brièvement la couche de présentation. Contrairement aux PetShopDNG précédents dans lesquels nous avions tenté de simuler le comportement d'un portail dynamique, chose malaisée avec le framework ASP.NET 1.0, nous avons opté ici pour une technique plus simple et plus directe:

A ceci près, le site est des plus classiques: il s'appuie sur la couche de services distribuée pour déclencher les différents cas d'utilisation.

2.2. Deux mots d'AOP

Vous l'aurez compris, ce n'est ni l'architecture ni la conception détaillée du PetShopAOP qui en font l'intérêt. Celles-ci sont en effet complètement banales aujourd'hui. Non, l'intérêt est ailleurs: dans l'utilisation de la Programmation Orientée Aspect (AOP) pour simplifier au maximum l'implémentation de chaque portion de l'application. Mais avant de nous lancer dans un tissage effréné, revoyons ensemble les bases essentielles de cette technologie, et disons deux mots sur son implémentation statique dans AspectDNG.

2.2.a. Pour ceux qui ont manqué le début...

Faisons court: la Programmation Orientée Aspect est une technique complémentaire à la programmation traditionnelle (Objet ou procédurale). Prenons une application normale, développée en C# et compilée sous forme d'une Assembly; dans notre contexte, appelons cette application le PetShop de base ou PetShop cible. Grâce à l'AOP, il est tout à fait possible de:

Par exemple, un Aspect peut encapsuler (intercepter) l'appel à une méthode particulière et en profiter pour gérer d'éventuelles exceptions. Ou déclencher systématiquement un traitement avant et/ou après l'invocation d'une méthode donnée.

D'autre part, rien n'interdit à un Aspect de décrire des modifications structurelles telles que la spécification d'attributs, de méthodes voire de relations d'héritage ou d'implémentation d'interfaces. Le tissage d'un tel Aspect aura donc pour effet d'ajouter de nouveaux attributs/méthodes à des classes qui ne les avaient pas définis initialement ou encore d'implémenter une interface après coup, limitant ainsi le couplage apparent (ie le couplage de la classe telle qu'elle aura été développée de manière artisanale, avant tissage).

Invocations de méthodes "récurrentes", attributs "copiés collés" sur plusieurs classes, relations d'implémentation obligatoires... Tout cela peut être extrait du code cible pour être centralisé dans un Aspect et surtout n'être développé qu'une fois pour toute.

2.2.b. AspectDNG

AspectDNG est un tisseur d'Aspects statique. Il fonctionne après la compilation, directement sur des assemblies .NET, ce qui permet de développer les applications cible et les Aspects dans n'importe quel langage de programmation compilable en Assembly. Comme il n'existe a priori aucun rapport (aucun lien, aucune dépendance) entre les Aspects et le code cible, AspectDNG permet de spécifier " nous souhaitons tisser" et "ce que nous souhaitons tisser" par le biais d'expressions XPath ou via un langage simplifié proche des expressions régulières.

D'autre part, cette spécification de tissage peut être exprimée dans un fichier XML externe à la fois aux Aspects et au code cible (de manière à conserver un découplage total), ou encore dans le code des Aspects par l'utilisation d'Attributes .NET (méta-données). Cette seconde technique pollue un peu les Aspects et limite leur réutilisation, mais s'avère plus simple et plus directe à l'usage; elle est donc à préférer pour les Aspects contextuels, propres à un projet donné. Découpler totalement Aspects et code cible n'est indispensable que lorsqu'on développe une bibliothèque d'Aspects réutilisables, c'est-à-dire tissables sur n'importe quel code cible.

Pour démystifier le fonctionnement interne d'AspectDNG, sachez qu'il procède de la manière suivante:

Figure 3. Tissage schématique

Vous l'aurez compris, dès qu'une assembly a été désassemblée en XML, tout devient beaucoup plus simple. Le tissage n'est qu'un ensemble de transformations du document avant réassemblage. Dans ses premières versions, AspectDNG reposait sur XSLT pour implémenter ces transformations (nous appelions ces transformations des méta-aspects puisque leur rôle était de tisser, ou d'instancier, un Aspect dans un contexte donné). Mais pour des raisons de performances et de maintenabilité, nous avons récemment réécrit cette partie en C# et XPath. Ce qui ne change absolument rien pour l'utilisateur d'AspectDNG.

Une mise en garde toutefois: AspectDNG est encore un projet expérimental, nous n'avons pas encore atteint la stabilité en grande partie à cause d'une bibliothèque utilitaire permettant l'assemblage/désassemblage des assemblies, que nous sommes en train de réécrire en parallèle de la correction des bugs sur la partie transformation pure. Nous prévoyons d'atteindre la version 1.0 d'ici à l'été 2005 sauf surprise particulière. Ce qui signifie qu'AspectDNG fonctionne bien aujourd'hui, que la plupart des fonctionnalités sont déjà implémentées, mais qu'il existe certaines limitations sur les types d'assemblies tissables; ces limitations disparaîtront dans les mois à venir.

Nous l'avons mentionné plus haut, AspectDNG est un "tisseur statique", c'est-à-dire qu'il procède aux modifications des assemblies cibles avant leur exécution. Cela présente deux avantages notables:

3. Etat des lieux

Dans cette partie, nous nous proposons de faire la part des choses entre le code que nous acceptons encore de développer manuellement et celui qui doit être externalisé dans un ou plusieurs Aspects. L'objectif est multiple:

Enfin, une précision avant d'entrer dans le vif du sujet: nous n'allons travailler ici que sur les couches de service, du domaine et de l'accès aux données. Nous laisserons la couche de présentation pour nous consacrer au "backend". En effet, le code exécutable de la couche de présentation du PetShop est quasiment inexistant (environ 100 lignes de code C# si l'on exclut celles générées automatiquement par l'outil de conception visuelle des pages).

3.1. Le code utile

3.1.a. Domaine

Le coeur du PetShop est constitué par ses objets du domaine. Il s'agit de classes C# particulièrement simples qui ne dépendent d'aucune autre couche de l'application et bien entendu d'aucune bibliothèque technique. Nous nous interdisons ici toute adhérence vis-à-vis de NHibernate, .NET Remoting ou log4net par exemple. Voici le diagramme de classe du modèle du domaine:

Figure 4. Diagramme de classes du domaine

Le code quant à lui est trivial et peut être déduit du diagramme précédent par un générateur automatique. Prenons l'exemple de la classe "Product":

01. using System.Collections;
02.
03. namespace PetShopAOP.Domain {
04.      public class Product {
05.          public Product(Category cat) {
06.              m_Category = cat;
07.              m_Category.Products.Add(this);
08.          }
09.          public Product(Category cat, string name, string description) : this(cat) {
10.              m_Name = name;
11.              m_Description = description;
12.          }
13.
14.          private string m_Name;
15.          public string Name {
16.              get { return m_Name; }
17.              set { m_Name = value; }
18.          }
19.
20.          private string m_Description;
21.          public string Description {
22.              get { return m_Description; }
23.              set { m_Description = value; }
24.          }
25.
26.          private Category m_Category;
27.          public Category Category {
28.              get { return m_Category; }
29.              set { m_Category = value; }
30.          }
31.
32.          private IList m_Items = new ArrayList();
33.          public IList Items {
34.              get { return m_Items; }
35.              set { m_Items = value; }
36.          }
37.      }
38. }
Product.cs

3.1.b. Services

Les fonctionnalités du PetShop sont très limitées. Elles portent sur deux principaux cas d'utilisation:

On se rend vite compte que le PetShop n'a quasiment aucune intelligence fonctionnelle. L'essentiel de ses activités porte sur la gestion de données, persistantes ou non. Nous pouvons donc nous permettre de reproduire ici la totalité du code de la couche de service, c'est-à-dire de nos deux classes jouant le rôle de contrôleurs de cas d'utilisation.

Tout d'abord, le contrôleur d'interaction avec le catalogue d'animaux en vente:

01. using System.Collections;
02. using PetShopAOP.Domain;
03. using PetShopAOP.Data;
04.
05. namespace PetShopAOP.Service {
06.      public class CatalogService {
07.          // Finder services
08.          public IList FindAll() {
09.              return CategoryDAO.Instance.FindAll();
10.          }
11.
12.          public Category FindCategoryByName(string name) {
13.                         return CategoryDAO.Instance.FindByName(name);
14.          }
15.
16.          public IList FindAllItemsAbout(string text) {
17.              return ItemDAO.Instance.FindAllAbout(text);
18.          }
19.
20.                  // Load by ID services
21.          public Category LoadCategory(object id) {
22.              return CategoryDAO.Instance.Load(id);
23.          }
24.
25.          public Product LoadProduct(object id) {
26.              return ProductDAO.Instance.Load(id);
27.          }
28.
29.          public Item LoadItem(object id) {
30.              return ItemDAO.Instance.Load(id);
31.          }
32.
33.          // Save services
34.          public Category Save(Category c) {
35.                        return CategoryDAO.Instance.Save(c);
36.          }
37.
38.          // Delete services
39.          public void DeleteCategory(object id) {
40.              CategoryDAO.Instance.Delete(id);
41.          }
42.
43.          public void Delete(Category c) {
44.              CategoryDAO.Instance.Delete(c);
45.          }
46.      }
47. }
CatalogService.cs

Puis le contrôleur du cas d'utilisation "gestion des comptes utilisateur":

01. using System;
02. using System.Collections;
03. using PetShopAOP.Domain;
04. using PetShopAOP.Data;
05.
06. namespace PetShopAOP.Service {
07.      public class AuthenticationException : ApplicationException{}
08.      public class UserAlreadyExistException : ApplicationException{}
09.
10.      public class AccountService {
11.          public Account TryToLogin(string login, string password) {
12.              Account result = AccountDAO.Instance.FindAccount(login, password);
13.              if (result == null){
14.                  throw new AuthenticationException();
15.              }
16.              return result;
17.          }
18.
19.          public Account TryToCreateNewAccount(string login, string password) {
20.              Account result = null;
21.              if (AccountDAO.Instance.CountAccounts(login) == 0){
22.                  result = new Account(login, password);
23.                  Save(result);
24.              }
25.              else{
26.                  throw new UserAlreadyExistException();
27.              }
28.              return result;
29.          }
30.
31.          public IList FindAllAccounts() {
32.              return AccountDAO.Instance.FindAll();
33.          }
34.
35.          public Account Save(Account a) {
36.                          return AccountDAO.Instance.Save(a);
37.          }
38.
39.          public Account Load(object id) {
40.              return AccountDAO.Instance.Load(id);
41.          }
42.
43.          public void Delete(object id) {
44.              AccountDAO.Instance.Delete(id);
45.          }
46.
47.          public void Delete(Account a) {
48.              AccountDAO.Instance.Delete(a);
49.          }
50.      }
51. }
AccountService.cs

Comme vous l'aurez noté, la couche de service dépend à la fois de la couche d'accès aux données et des objets du domaine. Par contre, heureusement, nous ne voyons toujours pas apparaître de couplage vis-à-vis d'une quelconque infrastructure technique (.NET Remoting, NHibernate...).

3.1.c. Accès aux données

La couche d'accès aux données est vouée à être dépendante d'un socle technique. Elle peut utiliser directement sur ADO.NET pour se connecter aux bases de données ou s'appuyer sur une surcouche d'ADO.NET. C'est le choix que nous avons fait dans ce PetShopAOP: nous utiliserons NHibernate pour effectuer le mapping entre les objets persistants et les structures relationnelles permettant la sauvegarde de leur état.

En termes de conception, pour faciliter la compréhension du modèle de stockage aux autres couches (typiquement à la couche de service), nous implémenterons ici un DAO (Data Access Object) chargé d'implémenter les opérations de base de recherche, création, suppression et modification pour chaque type d'objet persistant.

Du coup, il faut être prêt à voir apparaître dans chaque DAO une dépendance forte vis-à-vis de l'API NHibernate. Voici le code typique que nous aimerions pouvoir écrire:

01. using System;
02. using System.Collections;
03. using PetShopAOP.Domain;
04. using NHibernate;
05.
06. namespace PetShopAOP.Data {
07.      public class ProductDAO{
08.          private ISession m_session;
09.
10.          public Product Save(Product p) {
11.              m_session.Save(p);
12.              return p;
13.          }
14.
15.          public Product Load(object id) {
16.              return (Product) m_session.Load(typeof(Product), Convert.ToInt32(id));
17.          }
18.
19.          public void Delete(object id) {
20.              m_session.Delete("from Product where id = ?", id, NHibernate.NHibernate.Int32);
21.          }
22.
23.          public void Delete(Product c) {
24.              m_session.Delete(c);
25.          }
26.      }
27. }
ProductDAO.cs

Ce code est assez simple et centralise l'utilisation de l'outil de mapping Objet/Relationnel. Si nous devions en changer plus tard, il n'y aurait de répercussions que dans nos DAO.

3.2. Qu'avons-nous oublié?

Dans la section précédente, il semblerait que nous ayons implémenté le strict nécessaire mais que de nombreux besoins aient été complètement oubliés.

Il suffit de se remettre en tête notre spécification d'architecture pour nous apercevoir des oublis concernant les services techniques:

Et bien entendu, certains services techniques "de support" sont également importants et souvent omis ou sous-spécifiés par la spécification technique (c'est exactement ce que nous avons fait!):

Enfin du point de vue de la conception Objet un certain nombre de précautions n'ont pas été prises. En particulier, on ne passe pas par une AbstractFactory pour récupérer une référence vers l'interface d'un DAO. Comment migrer d'une implémentation à une autre des DAO sans ce découplage?

Aucun de ces "oublis" n'est anodin. Toutes ces choses sont indispensables à notre application mais les implémenter directement dans le code artisanal constituerait une pollution importante. Cela serait nuisible à:

Non, vous l'avez compris, tout ceci est motivé: nous allons gérer chacun des points mentionnés en tissant plusieurs Aspects sur le PetShop de base.

4. Le métier à tisser

Dans cette section, nous allons passer en revue tous les Aspects mis en oeuvre dans le PetShopAOP. Pour les premiers, nous serons décrirons à la fois le comportement du tisseur et l'objectif des Aspects; puis cela vous semblera de plus en plus naturel donc nous finirons par nous focaliser sur les Aspects et ferons passer AspectDNG au second plan.

Nous irons du tissage le plus simple au plus subtil. Ce n'est pas forcément l'approche la plus logique d'un point de vue architectural mais cela nous permettra de nous approprier progressivement les outils, conceptuellement et syntaxiquement.

4.1. Objets non identifiés

Certains objets du domaine vont être rendus persistants par NHibernate. A cette fin, il est indispensable que chacun dispose d'une propriété (ou du moins d'un attribut) stockant un identifiant unique utilisé pour assurer la cohérence entre le modèle objet en mémoire et le modèle de stockage relationnel en base de données. Dans certaines situations, les couches utilisatrices (typiquement la couche de présentation) auront certainement besoin d'avoir accès en lecture à cette information donc disposer d'une propriété (readonly) ne fera pas de mal. Il pourrait également s'avérer intéressant de redéfinir le comportement de la méthode GetHashCode() pour qu'elle renvoie également cette valeur unique (il existe quelques limitations à cette pratique mais ne nous attardons ici pas sur ces détails).

Si nous regroupons dans une classe tout ce que nous aimerions "ajouter" aux objets du domaine, cela pourrait donner quelque chose de ce genre:

01. namespace PetShopAOP.Aspects {
02.      public class Domain {
03.          private int m_Id;
04.
05.                  public int Id {
06.              get { return m_Id; }
07.          }
08.
09.          public override int GetHashCode() {
10.              return m_Id;
11.          }
12.      }
13. }
Domain.cs

Il ne reste plus qu'à indiquer à AspectDNG que nous souhaitons greffer ces membres (m_Id, Id et GetHashCode()) sur les objets du domaine. Cela pourrait se faire dans le code précédent en ajoutant quelques Attributes, mais ce serait redondant. En réalité, nous souhaitons greffer tous les membres de la classe Domain sur tous les objets du domaine. Ce genre de besoin doublement ensembliste ne peut être exprimé que de manière externe, dans le fichier de configuration AspectDNG.xml dont voici un extrait:

01. <!DOCTYPE AspectDngConfig[
02.      <!ENTITY ServiceTypes "//Type[@namespace='PetShopAOP.Service'][contains(@name, 'Service')]">
03.      <!ENTITY DAOTypes "//Type[@namespace='PetShopAOP.Data'][contains(@name, 'DAO')]">
04.      <!ENTITY DomainTypes "//Type[@namespace='PetShopAOP.Domain']">
05.      <!ENTITY AspectTypes "//Type[@namespace='PetShopAOP.Aspects']">
06. ]>
07. <AspectDngConfig xmlns="http://www.dotnetguru.org/AspectDNG"
08.      debug="false"
09.       logIlml="true" logIlmlPath="../Service/bin/ilml-log.xml"
10.      logWeaving="true" logWeavingPath="../Service/bin/weaving-log.xml">
11.      <BaseAssembly>../Service/bin/PetShopAOP.exe</BaseAssembly>
12.      <AspectsAssembly>../Service/bin/PetShopAOPAspects.dll</AspectsAssembly>
13.
14.          <Advice>
15.          <Insert aspectXPath="&AspectTypes;[@name='Domain']/*" targetXPath="&DomainTypes;"/>
16.
17.                              <!-- ... -->
18.      </Advice>
19. </AspectDngConfig>
AspectDNG.XML (version 1, partiel)

Dès lors, chaque objet du domaine peut être rendu persistant par NHibernate. Simple, non? Et cela nous aura évité de copier/coller ces membres manuellement dans chaque classe persistante.

4.2. Infrastructure de distribution

Dans notre diagramme d'architecture technique, nous avons prévu de distribuer la couche de service. Une question se pose dès lors: quels types de données échange la couche de service avec la couche de présentation? Voici quelques temps, le réflexe à la mode aurait été de placer le Design Pattern DTO (Data Transfer Object). Mais celui-ci a récemment été très critiqué: appliqué de manière systématique, il n'avait pas forcément de valeur ajoutée importante pour chaque type de donnée échangé par rapport à un graphe d'objets sérialisés; or son élaboration et surtout sa maintenance peuvent s'avérer coûteuses. Nous avons donc choisi ici de nous passer de ce Pattern et de rendre simplement les objets du domaine sérialisables.

Il nous faut donc répondre à deux besoins particuliers:

Il est très simple de régler le premier: ce n'est qu'une petite extension du tissage précédent. Jetons un oeil sans détour:

01. <!DOCTYPE AspectDngConfig[
02.      <!ENTITY ServiceTypes "//Type[@namespace='PetShopAOP.Service'][contains(@name, 'Service')]">
03.      <!ENTITY DAOTypes "//Type[@namespace='PetShopAOP.Data'][contains(@name, 'DAO')]">
04.      <!ENTITY DomainTypes "//Type[@namespace='PetShopAOP.Domain']">
05.      <!ENTITY AspectTypes "//Type[@namespace='PetShopAOP.Aspects']">
06. ]>
07. <AspectDngConfig xmlns="http://www.dotnetguru.org/AspectDNG"
08.      debug="false"
09.       logIlml="true" logIlmlPath="../Service/bin/ilml-log.xml"
10.      logWeaving="true" logWeavingPath="../Service/bin/weaving-log.xml">
11.      <BaseAssembly>../Service/bin/PetShopAOP.exe</BaseAssembly>
12.      <AspectsAssembly>../Service/bin/PetShopAOPAspects.dll</AspectsAssembly>
13.
14.          <Advice>
15.          <!-- Make Domain classes serializable and insert the ID management on each of them -->
16.          <Insert aspectXPath="&AspectTypes;[@name='Domain']/*" targetXPath="&DomainTypes;"/>
17.                      <MakeSerializable targetXPath="&DomainTypes;"/>
18.      </Advice>
19. </AspectDngConfig>
AspectDNG.XML (version 2, partiel)

Pour ce qui est de rendre les services distribués, décomposons le besoin en étapes plus simples. Il nous faut:

Cette fois, notre besoin de tissage n'est pas doublement ensembliste ni très réutilisable. Nous allons donc spécifier les étapes du tissages dans des Attributes situés dans la classe d'Aspect elle-même. Jugez plutôt:

01. using System;
02. using System.Configuration;
03. using System.Reflection;
04. using System.Runtime.Serialization;
05. using System.Runtime.Remoting;
06. using System.Runtime.Remoting.Channels;
07. using System.Runtime.Remoting.Channels.Tcp;
08. using System.Runtime.Remoting.Channels.Http;
09. using PetShopAOP.Service;
10. using AspectDNG;
11.
12. namespace System{
13.      [SetBaseType(PetShopAOP.Aspects.Constants.ServiceClassesXPath)]
14.      public class MarshalByRefObject {
15.         }
16. }
17.
18. namespace PetShopAOP.Aspects {
19.      [MakeSerializable(Constants.ExceptionsXPath)]
20.      public class DistributedException{
21.          [Insert(Constants.ExceptionsXPath)]
22.          protected DistributedException(SerializationInfo si, StreamingContext sc){}
23.           }
24.
25.      public class Server{
26.          [Insert(Constants.EntryPointClassXPath)]
27.          public static void InitRemotingServices(){
28.              int tcpPort = int.Parse(ConfigurationSettings.AppSettings["server.tcp.port"]);
29.              int httpPort = int.Parse(ConfigurationSettings.AppSettings["server.http.port"]);
30.
31.              ChannelServices.RegisterChannel(new TcpChannel(tcpPort));
32.              ChannelServices.RegisterChannel(new HttpChannel(httpPort));
33.
34.              Type sampleServiceType = typeof(CatalogService);
35.              foreach(Type t in sampleServiceType.Assembly.GetTypes()){
36.                  if (t.BaseType == typeof(MarshalByRefObject)){
37.                      RemotingConfiguration.RegisterWellKnownServiceType(t, t.Name, WellKnownObjectMode.Singleton);
38.                  }
39.              }
40.
41.              Console.WriteLine("Server started");
42.              Console.WriteLine("Hit <<Enter>> to stop");
43.              Console.ReadLine();
44.          }
45.
46.          [InlineBeforeReturn(Constants.EntryPointClassXPath + "/Method[@name='Main']")]
47.          public static void StartServer(){
48.              InitRemotingServices();
49.          }
50.      }
51. }
DistributedService.cs

Profitons-en pour consulter la classe dans laquelle nous avons consigné toutes les expressions XPath utilisées par les Attributs de tissage:

01. using System;
02.
03. namespace PetShopAOP.Aspects {
04.      public class Constants {
05.          public const string DomainAndServiceClassesXPath = "xpath: //Type[@namespace='PetShopAOP.Domain' or (@namespace='PetShopAOP.Service' and contains(@name, 'Service'))]";
06.          public const string ServiceClassesXPath = "xpath: //Type[@namespace='PetShopAOP.Service'][contains(@name, 'Service')]";
07.          public const string DAOClassesXPath = "xpath: //Type[@namespace='PetShopAOP.Data'][contains(@name, 'DAO')]";
08.          public const string EntryPointClassXPath = "xpath: //Type[@fullName='PetShopAOP.Service.MainClass']";
09.          public const string ExceptionsXPath = "xpath: //Type[starts-with(@namespace,'PetShopAOP') and contains(@name, 'Exception')]";
10.      }
11. }
Constants.cs

Maintenant que nous disposons de tous les éléments, quelques explications sur l'Aspect DistributedService ne feront pas de mal:

A l'issue de ce tissage un peu plus élaboré que le précédent, nos services sont devenus distribués et le lancement du serveur est aussi simple que... l'exécution de l'assembly .EXE elle-même puisque sa méthode Main() a été modifiée et s'occupe de tout.

Bien sûr, certains points sont contestables dans ce tissage simpliste. Par exemple, nous nous sommes permis de modifier la relation d'héritage des services en partant du principe que tous héritaient directement de la classe System.Object. Rediriger l'héritage n'a donc pas de répercussion sur le comportement nominal des services. Mais dans l'absolu, il faudrait parcourir le graphe d'héritage et ne rediriger vers MarshalByRefObject que les services dont le père est System.Object, ce qui leur permettrait de tirer parti de l'héritage traditionnel malgré le tissage de notre Aspect.

4.3. Optimisation

Dans notre application, certains services risquent d'être sollicités très souvent: en particulier les services de requétage sur le catalogue d'animaux. Nous pourrions "être fainéants" et nous reposer sur les mécanismes de cache de la base de données pour optimiser cela mais nous subirions systématiquement le mapping Objet/Relationnel! Nous pourrions dans ce cas faire confiance à NHibernate qui gère un cache objet en mémoire mais nous traverserions tout de même la couche de service, d'accès aux données et nous solliciterions NHibernate.

Nous pouvons faire mieux et à peu de frais: il suffirait en première approximation de stocker les résultats des invocations de méthodes dans une structure de cache temporaire pendant une certaine période (10 secondes, 20 minutes: à définir). Implémenter ce cache peut être aussi simple que de partager une Hashtable. Mais comment faire en sorte que chaque méthode de recherche:

Eh bien il suffit d'intercepter l'invocation à toutes les méthodes de recherche dans les classes de service, et d'implémenter cette logique nous-mêmes dans un Aspect avant de propager l'invocation de la méthode visée ou au contraire de court-circuiter son invocation et de renvoyer le résultat de l'invocation précédente. Prêts? Allons-y:

01. using System;
02. using System.Text;
03. using System.Collections;
04. using System.Reflection;
05. using System.Configuration;
06. using PetShopAOP.Data;
07. using AspectDNG;
08. using NHibernate;
09.
10. namespace PetShopAOP.Aspects {
11.      [Insert("")]
12.      public class CacheManager {
13.          public readonly static IDictionary Cache = new Hashtable();
14.          public readonly static int Period = 10;
15.
16.          static CacheManager(){
17.              Period = int.Parse(ConfigurationSettings.AppSettings["cache.period.in.seconds"]);
18.          }
19.
20.      }
21.
22.      public class Optim {
23.          [GenericAroundBody(Constants.ServiceClassesXPath + "/Method[starts-with(@name, 'Find') or starts-with(@name, 'Load')][not(contains(@name, '_'))]")]
24.          public object CacheLastResult(object[] data, MethodBase targetOperation){
25.              object result = null;
26.
27.              // Create a full name of the operation, including parameters values
28.              // since the operation may have different results for different parameters
29.              StringBuilder buf = new StringBuilder(targetOperation.DeclaringType + targetOperation.Name);
30.              foreach(object param in data){
31.                  buf.Append(param);
32.              }
33.              string fullName = buf.ToString();
34.              string fullNameLastExec = fullName + "-lastExec";
35.
36.              // Decide to re-execute the target operation or not
37.              bool shouldExecute = ! CacheManager.Cache.Contains(fullNameLastExec);
38.              if (! shouldExecute){
39.                  DateTime lastExec = (DateTime) CacheManager.Cache[fullNameLastExec];
40.                  shouldExecute = ((DateTime.Now - lastExec).Seconds > CacheManager.Period);
41.              }
42.
43.              if (shouldExecute){
44.                  result = targetOperation.Invoke(this, data);
45.              }
46.
47.              // Only lock the Cache to get or set data.
48.               // Hence, in fact some (few) threads may run the operation in parallel with the same parameters,
49.               // but this is better for parallelism
50.              lock(CacheManager.Cache){
51.                  if (shouldExecute){
52.                      CacheManager.Cache[fullNameLastExec] = DateTime.Now;
53.                      CacheManager.Cache[fullName] = result;
54.                  }
55.                  else{
56.                      result = CacheManager.Cache[fullName];
57.                  }
58.                         }
59.
60.              return result;
61.          }
62.
63.          [InlineAtStart(Constants.ServiceClassesXPath + "/Method[not(starts-with(@name, 'Find') or starts-with(@name, 'Load'))][not(contains(@name, '_'))]")]
64.          public void InvalidateCacheFromService(){
65.              CacheManager.Cache.Clear();
66.          }
67.      }
68. }
Optim.cs

Vous avez tout compris:

Un argument revient souvent dans les débats traitant de ce genre de technique d'optimisation: finalement, ne perdons-nous pas plus de temps ou d'espace mémoire à mettre en oeuvre ces optimisations plutôt que de ne pas le faire, tout simplement? Eh bien n'hésitons plus: grâce à l'Aspect précédent, activer ou désactiver le mécanisme de cache ne coûte qu'un tissage, aucune modification de code!

4.4. Sessions, transactions et exceptions

En apercevant tout à l'heure le code d'un DAO, vous vous êtes certainement demandé: "mais quand est-ce que les Sessions NHibernate sont initialisées? Je vois bien que le code utilise une Session, mais à aucun moment on ne gère son cycle de vie".

Mais en y réfléchissant, la notion de Session NHibernate est indissociable de celle de Transaction et de la gestion des exceptions. En effet, si un service donne lieu à des modifications persistantes par le bais des DAO, il est important que cela se fasse dans une transaction unique. Et donc qu'une Session ait déjà été ouverte. D'autre part, si au cours de l'exécution d'un service un DAO déclenche une exception, il est important que toutes les modifications effectuées au préalables soient elles-aussi annulées, de manière transactionnelle. Bref, ces trois facettes techniques sont liées.

Voici les principes que nous avons retenus:

Enfin, puisque les Sessions ne sont plus gérées au niveau des DAO, il faut trouver une technique pour donner à chaque DAO le moyen de manipuler la Session courante, initialisée par un service englobant. De façon plus générale, tout acteur déclenché directement ou indirectement par un Service doit pouvoir manipuler la Session courante. La technique généralement employée pour partager une ressource technique entre tous les acteurs traversés par un graphe d'appel est de placer cette ressource sur le Thread Local Storage (TLS); ainsi, associée au Thread, la ressource l'accompagne et reste disponible quel que soit la méthode en cours d'exécution.

4.4.a. Cycle de vie

Mettons tout cela en pratique: nous avons deux Aspects à tisser, le premier gérant le cycle de vie des Sessions et des Transactions NHibernate, le second donnant accès à la Session courante à chaque DAO par le biais de son attribut m_session. Pour simplifier le travail du premier, nous nous appuierons sur une classe utilitaire, NHibernateHelper, qui se charge le paramétrage de NHibernate, crée automatiquement le schéma de la base de données et enfin initialise une Fabrique de Sessions NHibernate:

01. using System;
02. using NHibernate;
03. using NHibernate.Cfg;
04. using NHibernate.Tool.hbm2ddl;
05.
06. namespace PetShopAOP.Data {
07.      public class NHibernateHelper {
08.          // No multithreading problem here, no need to double check and lock
09.          private static NHibernateHelper m_Instance;
10.          public static NHibernateHelper Instance{
11.               get{ return (m_Instance == null) ? m_Instance = new NHibernateHelper("GlobalModel.hbm.xml") : m_Instance; }
12.          }
13.          private NHibernateHelper(){}
14.
15.          public readonly ISessionFactory m_factory;
16.
17.          private NHibernateHelper(string configPath){
18.              Configuration m_config = new Configuration();
19.              m_config.AddXmlFile(configPath);
20.                      m_factory = m_config.BuildSessionFactory();
21.
22.              // Create the database schema
23.                         SchemaExport export = new SchemaExport(m_config);
24.              export.SetOutputFile("generated-database-schema.sql");
25.              export.Create(false, true);
26.          }
27.      }
28. }
NHibernateHelper.cs

Notre premier Aspect s'appuie sur cette classe pour gérer le cycle de vie des Sessions:

01. using System;
02. using System.Collections;
03. using System.Threading;
04. using System.Reflection;
05. using PetShopAOP.Data;
06. using AspectDNG;
07. using NHibernate;
08.
09. namespace PetShopAOP.Aspects {
10.      [Insert("")]
11.      public class NHibernateSessionManager{
12.          [ThreadStatic]
13.          public static ISession Session;
14.      }
15.
16.      public class NHibernateUser {
17.          // This m_session will be defined on target objects
18.          [ThreadStatic]
19.          private ISession m_session;
20.
21.          [InlineAtStart(Constants.DAOClassesXPath + "/Method[not(contains(@name, '_'))]")]
22.          public void NHibernateAutomaticSessionGetter(){
23.              m_session = NHibernateSessionManager.Session;
24.          }
25.
26.                  [GenericAroundBody(Constants.ServiceClassesXPath + "/Method[not(contains(@name, '_'))]")]
27.          public object NHibernateAutomaticSession(object[] data, MethodBase targetOperation){
28.              object result = null;
29.
30.              if (NHibernateSessionManager.Session == null){
31.                  ISession session = NHibernateHelper.Instance.m_factory.OpenSession();
32.                  m_session = NHibernateSessionManager.Session = session;
33.
34.                  // Start session and transaction. They will be closed at the same level.
35.                  ITransaction transaction = session.BeginTransaction();
36.                  try{
37.                      // Method invocation propagation
38.                      result = targetOperation.Invoke(this, data);
39.                      transaction.Commit();
40.                  }
41.                  catch(Exception e){
42.                      transaction.Rollback();
43.                      throw e;
44.                  }
45.
46.                  session.Flush();
47.                  session.Close();
48.                  m_session = NHibernateSessionManager.Session = null;
49.
50.              }
51.              else{
52.                  // Enlist this operation in the current session/transaction
53.                  m_session = NHibernateSessionManager.Session;
54.
55.                  // Simple propagation
56.                  result = targetOperation.Invoke(this, data);
57.              }
58.
59.              return result;
60.          }
61.
62.         }
63. }
NHibernateUser.cs

Concentrons-nous tout d'abord sur la méthode NHibernateAutomaticSession. Il s'agit d'un intercepteur qui sera déclenché à l'appel de toute méthode de service. S'il l'invocation provient de la couche de présentation, alors aucune Session NHibernate n'existe; l'intercepteur en initialise une, et en profite pour initialiser immédiatement une Transaction. Ce modèle est plus coûteux que le précédent car il implique une lecture transactionnelle; si un tel luxe de cohérence de données n'est pas indispensable, il serait trivial d'attendre la première modification (cette fois, au niveau d'un DAO) pour débuter cette Transaction.

Une fois les initialisations terminées, notre intercepteur propage l'invocation de méthode visée par la couche de présentation: targetOperation.Invoke(this, data) (à noter que le "this" sera l'objet sur lequel le tissage aura été opéré, c'est-à-dire le service). Puis à l'issue de cette propagation ferme la Transaction et la Session.

Dans le cas où la Session a déjà été initialisée, c'est nettement plus simple puisqu'il n'y a qu'à propager l'invocation de méthode. Vous remarquerez que dans les deux cas, l'attribut m_session est affecté et permet au service de manipuler la Session NHibernate. A priori, aucune manipulation explicite de cet attribut n'est nécessaire dans un service mais cela reste une éventualité.

4.4.b. Manipulation des Sessions

Jusqu'à présent, nous nous sommes focalisés sur la couche de Service qui gère le cycle de vie des Sessions. Mais une question reste sans réponse: comment feront les DAO pour interagir avec la Session courante? Devront-ils manipuler le TLS? Cela n'est-il pas un peu trop technique?

Non, vous allez voir qu'au contraire le code des DAO sera excessivement simple. Le seul besoin "technique" dans un DAO est de déclarer un attribut Session m_session. Souvenez-vous du code de ProductDAO, que nous rappelons ici pour nous raffraîchir la mémoire:

01. using System;
02. using System.Collections;
03. using PetShopAOP.Domain;
04. using NHibernate;
05.
06. namespace PetShopAOP.Data {
07.      public class ProductDAO{
08.          private ISession m_session;
09.
10.          public Product Save(Product p) {
11.              m_session.Save(p);
12.              return p;
13.          }
14.
15.          public Product Load(object id) {
16.              return (Product) m_session.Load(typeof(Product), Convert.ToInt32(id));
17.          }
18.
19.          public void Delete(object id) {
20.              m_session.Delete("from Product where id = ?", id, NHibernate.NHibernate.Int32);
21.          }
22.
23.          public void Delete(Product c) {
24.              m_session.Delete(c);
25.          }
26.      }
27. }
ProductDAO.cs

Rien de plus simple en effet. Mais vous l'aurez compris, cela signifie forcément que:

Tout ceci peut être pris en charge par un Aspect. Son premier rôle sera de déclarer un attribut Session m_session attaché au TLS sur tous les DAO. Si un attribut Session m_session existe déjà dans un DAO, il suffit de le supprimer et de le redéclarer convenablement. Voyons cela en étendant encore la configuration du tissage:

01. <!DOCTYPE AspectDngConfig[
02.      <!ENTITY ServiceTypes "//Type[@namespace='PetShopAOP.Service'][contains(@name, 'Service')]">
03.      <!ENTITY DAOTypes "//Type[@namespace='PetShopAOP.Data'][contains(@name, 'DAO')]">
04.      <!ENTITY DomainTypes "//Type[@namespace='PetShopAOP.Domain']">
05.      <!ENTITY AspectTypes "//Type[@namespace='PetShopAOP.Aspects']">
06. ]>
07. <AspectDngConfig xmlns="http://www.dotnetguru.org/AspectDNG"
08.      debug="false"
09.       logIlml="true" logIlmlPath="../Service/bin/ilml-log.xml"
10.      logWeaving="true" logWeavingPath="../Service/bin/weaving-log.xml">
11.      <BaseAssembly>../Service/bin/PetShopAOP.exe</BaseAssembly>
12.      <AspectsAssembly>../Service/bin/PetShopAOPAspects.dll</AspectsAssembly>
13.
14.      <Advice>
15.          <!-- Make Domain classes serializable and insert the ID management on each of them -->
16.          <MakeSerializable targetXPath="DomainTypes;"/>
17.          <Insert aspectXPath="&AspectTypes;[@name='Domain']/*" targetXPath="&DomainTypes;"/>
18.
19.                              <!-- Delete m_session fields from target objects (since they are not ThreadStatic) -->
20.          <Delete targetXPath="&ServiceTypes;/Field[@name='m_session']"/>
21.
22.                  <!-- Re-insert thread-static field -->
23.          <Insert aspectXPath="&AspectTypes;[@name='NHibernateUser']/Field[@name='m_session']" targetXPath="&ServiceTypes;"/>
24.          <Insert aspectXPath="&AspectTypes;[@name='NHibernateUser']/Field[@name='m_session']" targetXPath="&DAOTypes;"/>
25.      </Advice>
26. </AspectDngConfig>
AspectDNG.XML (version 3, partiel)

D'autre part, il faut s'assurer que m_session pointe bien sur la session courante. Mais nous l'avons déjà implémenté dans l'Aspect NHibernateUser:

01. namespace PetShopAOP.Aspects {
02.      public class NHibernateUser {
03.          // This m_session will be defined on target objects
04.          [ThreadStatic]
05.          private ISession m_session;
06.
07.          [InlineAtStart(Constants.DAOClassesXPath + "/Method[not(contains(@name, '_'))]")]
08.          public void NHibernateAutomaticSessionGetter(){
09.              m_session = NHibernateSessionManager.Session;
10.          }
11.              }
12. }
NHibernateUser.cs (partiel)

Une petite précision a toute son importance ici, elle concerne les performances: quel que soit le DAO, quelle que soit la méthode invoquée et ses paramètres, notre aspect se borne toujours à faire la même chose. Dans une telle situation, il est donc superflu de réifier la pile ou les paramètres d'invocation de la méthode en un tableau d'objets puisqu'on ne l'utiliserait pas. Nous attirons donc votre attention sur le nom de la technique de tissage : [InlineAtStart]. Son comportement est audacieux: il consiste à copier directement le corps de la méthode NHibernateAutomaticSessionGetter au début du corps de toutes les méthodes cibles (dans les DAO)! Ainsi, que nous écrivions du code une seule fois dans l'Aspect ou que nous le copions-collons dans toutes les méthodes de tous les DAO, les performances seront exactement les mêmes. D'ailleurs, si cela pique votre curiosité, n'hésitez pas à décompiler le code tissé et à vérifier cela par vous-mêmes. Nous n'avons besoin d'aspects génériques (à réification) que si leur comportement est contextuel, qu'il dépend des valeurs de paramètres des méthodes interceptées; dans les autres cas, tissez en mode "inline", les performances seront bien meilleures!

Faisons le point: Sessions, Transactions et Exceptions sont maintenant gérées automatiquement au niveau de la couche de Service, par un Aspect. Et du point de vue de l'utilisation dans nos DAO, il suffit de déclarer un attribut Session m_session et de l'utiliser dans nos méthodes; il sera initialisé automatiquement et pointera toujours vers la Session courante, elle-même stockée dans le TLS. Difficile de rendre le code artisanal plus simple!

4.5. Services de support

Vous savez tout des possibilités offertes par l'AOP et en particulier par AspectDNG. Forts de ces connaissances, rien ne vous empêcherait d'aller plus loin et d'implémenter bien d'autres services par simple tissage d'Aspects. En guise d'exemple, nous vous proposons de tracer automatiquement l'exécution du PetShopAOP (en nous appuyant sur le framework log4net, de manière à ce que le niveau de détail de ces traces reste finement paramétrable):

01. using System.Reflection;
02. using System.Collections;
03. using System.Text;
04. using AspectDNG;
05. using log4net;
06.
07. namespace PetShopAOP.Aspects {
08.      public class Traces {
09.          [GenericAroundBody(Constants.ServiceClassesXPath + "/Method[not(contains(@name, '_'))]")]
10.          public object TraceAtStart(object[] data, MethodBase targetOperation){
11.              // Get method name (Delegate methods may look like "FindAll_987987-esf9I987-...")
12.              string baseName = targetOperation.Name.Split('_')[0];
13.
14.              // Get method parameter values
15.              StringBuilder buf = new StringBuilder(baseName).Append("(");
16.              for(int i=0; i<data.Length; i++){
17.                  buf.Append(data[i]);
18.                  if (i < data.Length - 1){
19.                      buf.Append(", ");
20.                  }
21.              }
22.              buf.Append(")");
23.
24.              // Log using lo4net
25.              LogManager.GetLogger(GetType()).Info(buf);
26.
27.                // Delegate the method invocation
28.              return targetOperation.Invoke(this, data);
29.          }
30.      }
31. }
Traces.cs

Un peu plus subtil, nous nous sommes amusés à ajouter un mécanisme de statistiques en temps réel sur le PetShopAOP. En deux mots, cela consiste à compter le nombre d'invocations de méthodes effectuées au fur et à mesure du déroulement de l'application ainsi que le temps passé dans chacune d'entre elles. Les informations collectées sont stockées en mémoire, mais sont également rendues disponibles à distance par un nouveau service .NET Remoting lui aussi injecté par tissage. Du point de vue graphique, vous pourrez voir en pied de page du PetShopAOP un lien hypertexte qui mène au tableau récapitulant ces statistiques (ce n'est pas une fonctionnalité métier, nous sommes d'accord, le but n'était que d'offrir au technicien le moyen de voir quelles portions de l'application sont les plus sollicitées ou les plus gourmandes).

Voici l'Aspect gérant les statistiques:

01. using System;
02. using System.Collections;
03. using System.Reflection;
04. using AspectDNG;
05.
06. namespace PetShopAOP.Aspects {
07.      [Insert("")]
08.      [Serializable]
09.      public class MethodStat{
10.          private string m_Name;
11.          public string Name{ get{ return m_Name; } }
12.
13.          private Type m_DeclaringType;
14.          public Type DeclaringType{ get{ return m_DeclaringType; } }
15.
16.          public string FullName{ get{ return string.Format("{0}.{1}::{2}", m_DeclaringType.Namespace, m_DeclaringType.Name, m_Name); } }
17.
18.          private int m_NbInvocations;
19.          public int NbInvocations{ get{ return m_NbInvocations; } }
20.
21.                  private long m_TotalExecutionTimeMillis;
22.          public long TotalExecutionTimeMillis{ get{ return m_TotalExecutionTimeMillis; } }
23.
24.          public MethodStat(Type declaringType, string name){
25.              m_Name = name;
26.              m_DeclaringType = declaringType;
27.          }
28.
29.          public void Increment(long milliseconds){
30.              m_NbInvocations++;
31.              m_TotalExecutionTimeMillis += milliseconds;
32.          }
33.      }
34.
35.      public class BasicStats {
36.          [GenericAroundBody(Constants.DomainAndServiceClassesXPath + "/Method[not(contains(@name, '_'))]")]
37.          public object TimeElapsed(object[] data, MethodBase targetOperation){
38.              // Full names may look like "FindAll_987987-esf9I987-..."
39.              string baseName = targetOperation.Name;
40.              int index = baseName.IndexOf("_");
41.              if (index > -1){
42.                  baseName = baseName.Substring(0, index);
43.              }
44.
45.              long start = DateTime.Now.Ticks;
46.              object result = targetOperation.Invoke(this, data);
47.              long end = DateTime.Now.Ticks;
48.
49.              MethodStat newStat = new MethodStat(targetOperation.DeclaringType, baseName);
50.              lock(BasicStatsServer.Stats){
51.                  MethodStat stat = BasicStatsServer.Stats[newStat.FullName] as MethodStat;
52.                  if (stat == null){
53.                      stat = newStat;
54.                      BasicStatsServer.Stats[stat.FullName] = stat;
55.                  }
56.                  stat.Increment((end - start) / 10000);
57.              }
58.
59.              return result;
60.          }
61.      }
62.
63.      [Insert("")]
64.      public class BasicStatsServer : MarshalByRefObject {
65.          public static readonly IDictionary Stats = new Hashtable();
66.
67.          public ICollection GetStats(){
68.              return Stats.Values;
69.          }
70.      }
71. }
BasicStatsServer.cs

Le résultat graphique peut donner quelque chose de ce genre:

Figure 5. Backend Statistics

5. Installation et tests

Le PetShopAOP est l'application de référence de notre tisseur AspectDNG. Elle vient donc d'être intégrée à la distribution d'AspectDNG lui-même. Ainsi donc, pour l'installer, rien de plus simple:

Il ne vous reste plus qu'à:

Faites bon usage du PetShopAOP, son code est complètement ouvert et n'attend que vos critiques et vos optimisations! Bons tissages!