| PetShop Orienté Aspect par Thomas Gil (thomas.gil@dotnetguru.org) | ||
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.
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"):
une couche de présentation, implémentée en mode Web par un ensemble de pages ASPX accompagnées de leur CodeBehind.
une couche de services distribués par .NET Remoting, en attendant Indigo (heureusement, l'AOP rendra "gratuit" le passage de .NET Remoting à Indigo).
une couche d'objets du domaine, implémentée sous forme de simples classes C# indépendantes de toute autre couche (d'aucuns diront donc des "Poco")
une couche d'accès aux données qui s'appuie sur NHibernate pour effectuer le mapping des objets persistants en base de données relationnelle.
enfin la base de données elle-même, que nous avons choisi de faire s'exécuter en mémoire au niveau de la couche d'accès aux données, afin de simplifier la vie de ceux qui installeront et testeront le PetShopAOP. Peu de bases permettent ce mode d'exécution en .NET pour le moment; nous avons retenu Firebird, une base de données Open Source disponible à l'adresse suivante: http://firebird.sourceforge.net/
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:
chaque page est autonome, à ceci près qu'elle utilise quelques contrôles utilisateur statiques (pour l'en-tête et le pied de page, le moteur de recherche et certains encarts correspondant aux préférences de l'utilisateur).
seule la navigation de page en page a été "raffinée", c'est-à-dire que nous avons centralisé dans un fichier de configuration XML la cartographie complète du site et délesté d'autant la responsabilité des pages ou contrôles utilisateur.
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.
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.
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:
de développer certaines portions de code (souvent technique, parfois métier), appelées Aspects
de tisser ces portions de code aux endroits voulus dans le PetShop cible afin d'éviter de dupliquer la gestion de certaines facettes telles que les traces, les exceptions techniques, les transactions, la distribution, l'optimisation (parmi les plus classiques). Le verbe tisser peut être remplacé pour mieux nous représenter la chose par l'un des synonymes ou approchants: greffer, insérer, voire "copier coller automatiquement"...
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.
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 "où 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:
décompilation de l'assembly cible en un document XML (représentant namespaces, types, opérations, instructions, etc...)
décompilation de l'assembly contenant les Aspects en un second document XML
itération séquentielle sur toutes les règles de tissage, et modification progressive du document XML cible par insertions d'éléments provenant du document XML des Aspects.
réassemblage du document XML cible ainsi modifié en une Assembly .NET
![]() |
|
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:
Le tissage est plus robuste, on peut s'apercevoir d'erreurs dans la spécification du tissage, et les corriger assez tôt dans le cycle de développement
Les performances de l'assembly tissée sont optimales. Selon les techniques employées, ces performances peuvent être strictement les mêmes que si l'on avait développé artisanalement la totalité du code. Les seules déperditions en performances ont lieu lorsqu'un tissage est générique, c'est-à-dire qu'un même aspect doit encapsuler des invocations de méthodes de signatures différentes: dans ce cas, il faut procéder à la réification de la pile ou de la liste des paramètres sous forme d'un tableau de System.Object.
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:
Eviter au maximum la duplication de code, de telle sorte que la maintenance et les évolutions soient simplifiées
Simplifier au maximum le code cible: puisqu'il est développé de manière artisanale, le risque d'erreur est important et la simplicité est donc de mise. Dans certains projets, ce code est parfois suffisamment simple pour être généré automatiquement par d'autres techniques (génération de code, MDA, etc...)
Limiter au maximum le couplage entre le code cible artisanal et toute bibliothèque technique susceptible d'être remplacée dans les versions ultérieures de notre logiciel. Sans l'AOP, nous utilisons généralement certains Design Patterns pour obtenir ce découplage; vous verrez qu'en général le tissage évite d'avoir recours à ces techniques et limite d'autant plus la complexité du code artisanal.
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).
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 |
Les fonctionnalités du PetShop sont très limitées. Elles portent sur deux principaux cas d'utilisation:
L'utilisation du catalogue d'animaux domestiques à vendre (recherches, ajouts, suppressions, modifications des catégories, produits et items)
La gestion des comptes des utilisateurs (login, informations personnelles, préférences)
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...).
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.
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:
la couche de service n'est absolument pas distribuée par .NET Remoting.
certes nos DAO utilisent l'API NHibernate, mais quid de l'initialisation des sessions, de la gestion des transactions et des exceptions techniques?
si certains objets du domaine doivent être persistants, où est géré leur identifiant technique?
d'autre part, si les objets du domaine doivent être passés par valeur à travers un framework de distribution, pourquoi ne sont-ils pas sérialisables?
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!):
la gestion des traces
l'optimisation des services coûteux
les statistiques d'exécution de notre application
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 à:
la simplicité donc à la facilité qu'auraient de nouveaux membres d'un projet à appréhender le code
l'évolutivité car plus de code signifie souvent plus de modifications à apporter en cas d'évolution
la cohérence du code car des redondances apparaîtraient forcément si l'on devait gérer tout cela manuellement et les évolutions successives du logiciels risqueraient de briser la cohérence de ces redondances
la productivité puisqu'il y aurait plus de code à écrire
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.
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.
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.
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:
rendre les objets du domaine sérialisables
rendre les services distribués (par .NET Remoting ici mais le problème serait similaire avec d'autres infrastructures de distribution)
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:
faire en sorte que chaque service hérite de MarshalByRefObject
si les services sont susceptibles de lever des exceptions, elles doivent être rendues sérialisables tout comme les objets du domaine, mais elle doivent également disposer d'un constructeur particulier de manière à ce que l'infrastructure côté client puisse les désérialiser sans encombre (un constructeur prenant en paramètres SerializationInfo et StreamingContext).
enfin, il faudra bien sûr démarrer le serveur .NET Remoting, choisir un port d'écoute, un protocole de transport, un encodage... et se mettre en attente des requêtes des clients.
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:
[SetBaseType] permet de modifier la relation d'héritage des classes cibles. Ainsi, elles hériteront de MarshalByRefObject et pourront être distribuées par .NET Remoting
Nous avions déjà rencontré [MakeSerializable] (dans le document AspectDNG.xml, mais l'effet est le même bien entendu), cette fois son objectif est de rendre les exceptions du niveau Service sérialisables
[Insert(Constants.ExceptionsXPath)] permet de greffer le constructeur nécessaire à la désérialisation sur toutes les exceptions du niveau Service
[Insert(Constants.EntryPointClassXPath)] nous permet de greffer, sans aucune modification, la méthode InitRemotingServices() sur la classe MainClass. Cette méthode procède au paramétrage de l'infrastructure .NET Remoting et à la mise en attente bloquante du Thread qui l'exécutera.
[InlineBeforeReturn(Constants.EntryPointClassXPath + "/Method[@name='Main']")] fait en sorte qu'après le comportement nominal de la méthode Main(), on déclenche l'invocation de InitRemotingServices() provoquant ainsi le lancement du serveur .NET Remoting.
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.
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:
stocke automatiquement son résultat dans le cache
et ne s'exécute que si les paramètres qu'on lui passe ne correspondent pas déjà à une entrée dans le cache, car dans ce cas il suffit de renvoyer la valeur mémorisée.
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:
La classe CacheManager est copiée sans modification particulière, et sera donc disponible dans l'assembly cible à l'exécution
La méthode CacheLastResult(object[] data, MethodBase targetOperation) encapsule toutes les invocations de méthodes (de recherche) de la couche service. Le premier paramètre data nous donne accès aux valeurs de tous les paramètres passés à la méthode cible, et le second targetOperation est une référence permettant de connaître et d'invoquer la méthode cible au moment voulu dans notre "méthode-intercepteur".
InvalidateCacheFromService() invalide notre cache dès qu'une méthode autre que de consultation est invoquée. C'est un peu pessimiste, il serait tout à fait possible de ne tisser cette invalidation que dans certains cas, à condition de bien connaître le comportement fonctionnel de notre application.
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!
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:
les Sessions NHibernates sont initialisées lors du déclenchement d'un service par la couche de présentation. Par contre, un service invoqué par un autre service réutilisera la même Session.
les Transactions sont initialisées par le premier DAO effectuant une opération de modification persistante. Toutes les autres sollicitations de DAO dans le cadre d'exécution du même service seront incluses dans cette transaction.
à la fin de l'exécution d'un service, si une Transaction est ouverte elle doit être validée (commit). Puis, dans tous les cas, la Session est à son tour validée puis fermée.
si une exception est levée par un DAO, un objet du domaine ou un service, le service de plus haut niveau (celui qui a été déclenché par la couche de présentation, et donc qui a initialisé la Session) doit annuler la Transaction en cours si elle existe et mettre fin à la Session.
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.
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é.
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:
cet attribut doit être multi-thread safe, ou plutôt attaché au Thread Local Storage (TLS) pour garantir la cohérence multi-tâche de notre application. Or nous n'avons pas déclaré m_session de manière à ce qu'il soit attaché au TLS! Que va-t-il se passer?
avant chaque invocation d'une méthode sur un DAO, il faut absolument s'assurer que l'attribut m_session pointe bel et bien sur la Session courante.
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!
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 |
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:
Téléchargez le code source de la dernière version (stable?) d'AspectDNG, disponible à cette adresse
Extrayez le ZIP dans le répertoire de votre choix. Disons c:\AspectDNG par exemple.
En ligne de commande, rendez-vous dans ce répertoire et lancez: build. Cela déclenche un script NAnt (NAnt est inclus dans la distribution d'AspectDNG)
Si tout se passe bien, félicitations! Vous avez re-construit AspectDNG, passé ses tests unitaires, reconstruit le PetShopAOP et également passé ses propres TU.
Il ne vous reste plus qu'à:
lancer le Backend (couche de service, domaine, accès aux données et base de données embarquée) en déclenchant: C:\\AspectDNG\AspectDNG\PetShopAOP\Service\bin\PetShopAOP.exe. Au bout de quelques secondes, un message devrait vous dire que le serveur est démarré, et qu'il vous suffira de taper <<Entrée>> pour l'arrêter.
déclarer un répertoire virtuel dans IIS:
Dans l'interface d'administration de IIS, choisissez <<Nouveau | Répertoire virtuel >> dans le menu dynamique
Alias (c'est important): PetShopAOPWeb
Répertoire: C:\\AspectDNG\AspectDNG\PetShopAOP\Web
lancer un navigateur Web et vous rendre à l'adresse suivante: http://localhost/PetShopAOPWeb/Welcome.aspx
Faites bon usage du PetShopAOP, son code est complètement ouvert et n'attend que vos critiques et vos optimisations! Bons tissages!