| Présentation du PetShop DotNetGuru par Thomas GIL (thomas.gil@valtech.fr) | ||
a
naissance du projet PetShopDNG est assez originale pour que nous la rappelions
brièvement
: souvenez-vous, cela remonte à l'époque où Microsoft venait de publier sa
propre implémentation du PetShop version 2.0. Ce PetShop dont nous avions analysé et
tant critiqué les nombreux choix d'architectures. Pour une application "de
référence", il est vrai que cela paraissait quelque peu exagéré ...
L'article de Sami Jaber ayant suscité de nombreuses réactions, nous avons tous deux beaucoup réfléchi à l'éventualité de réécrire complètement le PetShop, en suivant une architecture qui cette fois reflèterait les idées qui nous animent. Mais nous étions face à deux interrogations :
la première d'ordre métaphysique : était-ce bien à nous, DotNetGuru.org, d'implémenter une application multi-couche de référence sur .NET ? Cette tâche n'incombait-elle pas tout naturellement à l'éditeur de la plate-forme, c'est à dire Microsoft ?
la deuxième d'ordre organisationnel : le PetShopDNG prendrait probablement un certain temps à implémenter, non que l'application soit complexe, mais plutôt en raison des nombreux choix d'Architecture. Il fallait systématiquement trouver des compromis entre simplicité de mise en oeuvre, flexibilité et maintenabilité.
Un sondage sur DotNetGuru.org nous a permis de nous abstraire du premier questionnement. Le deuxième, vous vous en doutez : c'était un défi, nous nous demandions vraiment si c'était réalisable, mais comme ce projet nous tenait à coeur à tous deux... nous nous sommes embarqué dans cette fabuleuse aventure. Sami a donc endossé le rôle d'architecte expert pendant que je me consacrai à la conception et au développement du PetShopDNG.
Notre état d'esprit au démarrage du projet est assez bien décrit par les objectifs que nous nous étions fixés :
partager, sur un exemple simple et complet, nos idées sur l'architecture et la conception des applications multi-couches
donner sur DotNetGuru.org des exemples de code en prise avec un projet réel
exprimer nos recommandations à travers une application dite "de référence"
se faire plaisir en utilisant la plate-forme .NET et sa panoplie d'outils connexes
appliquer les principaux préceptes présentés tout au long de l'année passée dans les articles de DotNetGuru.org
lancer un débat d'idées autour des choix adoptés par notre PetShop, de manière à les critiquer et à éventuellement améliorer l'architecture cible.
Au contraire, nous souhaitions éviter de :
surenchérir dans la course aux performances. Le PetShopDNG n'a pas été optimisé, ses performances n'ont subi aucune mesure.
discréditer d'autres architectures ou implémentations du PetShop : de nombreux autres acteurs ont ou vont publier leur propre version du PetShop. DotNetGuru n'a pas l'exclusivité des bonnes idées sur l'architecture et la conception objet, bien heureusement !
Le PetShopDNG est donc une application multi-couches complète. Nous l'avons menée comme un véritable projet, dont nous retraçons ici les grandes lignes.
Le PetShop, en deux mots, est un site Web d'achat d'animaux (je n'ose pas dire "domestiques" à cause des reptiles). A cet effet, il permet naturellement de :
naviguer à travers un catalogue d'animaux disponibles (une catégorie d'animaux contient plusieurs produits, qui se déclinent en plusieurs articles)
rechercher l'ensemble des articles dont le nom contient une certaine chaîne de caractères
gérer un caddie virtuel, et permettre sur chaque article un ajout rapide dans le caddie
gérer un compte utilisateur (login, mot de passe, civilité de l'utilisateur...)
gérer certaines préférences (catégorie animale préférée, "quick links"...)
|
Figure A : Diagramme de cas d'utilisation |
Chaque cas d'utilisation aurait besoin d'être décrit précisément, mais comme vous avez déjà tous connaissance des fonctionnalités qu'ils recoupent, nous nous contenterons d'un diagramme d'activité global, qui résume bien les différentes fonctionnalités du site, ainsi que l'ordre dans lequel elles s'enchaînent.
|
Figure B : Diagramme d'activité de navigation dans le PetShopDNG |
Une petite explication s'impose. Le PetShopDNG est un portail, dont certaines Portlets sont statiques (c'est-à-dire présentes sur chaque page) :
sélection d'une catégorie
moteur de recherche
accès au compte utilisateur
accès au caddie virtuel
D'autres Portlets seront affichées dynamiquement, en fonction des demandes de l'utilisateur :
image cliquable présentant les différentes catégories d'animaux
liste des produits dans une catégorie
liste des articles dans un produit
authentification
caddie virtuel
compte utilisateur
Enfin, deux Portlets dépendent des préférences de l'utilisateur (stockées dans son compte) :
bannière représentant la catégorie préférée de l'utilisateur
accès rapide aux produits préférés (ceux de la catégorie préférée de l'utilisateur)
Le diagramme d'activité précédent nous indique donc le cheminement nominal d'un utilisateur qui part de l'état initial du portail, et qui navigue à travers le site jusqu'à venir ajouter des articles dans le caddie virtuel. Suivons-en un chemin possible :
[Optionnel] Authentification de l'utilisateur : passage obligé pour accéder au caddie virtuel (dans une Portlet dynamique)
Sélection d'une catégorie (dans la Portlet statique correspondante)
Sélection d'un produit (Portlet dynamique)
Sélection d'un article (Portlet dynamique)
Accès au caddie virtuel, où une ligne a été crée pour l'article sélectionné (Portlet dynamique). Exception : si la ligne existait déjà, son nombre d'article a été incrémenté de 1 bien entendu.
Les fonctionnalités du site sont assez simples, l'analyse le fut également. Nous vous épargnons les détails pour arriver directement au résultat de cette phase : le diagramme de classes décrivant notre modèle d'objets métier :
|
|
Si le PetShop est une application Web assez simple, son contenu lui, est très dynamique. Pour restreindre les responsabilités des pages de présentation au maximum, nous nous sommes attachés à organiser le site comme un portail, dont chaque composant est complètement indépendant des autres : c'est le principe récurrent de la Portlet.
Techniquement, sur la plate-forme .NET, une Portlet peut aisément s'implémenter sous la forme d'un contrôle utilisateur (ASCX). On pourra donc décider plus tard de rendre certaines Portlets systématiquement visibles, ou au contraire de les faire apparaître dynamiquement en fonction des souhaits de l'utilisateur.
Bien entendu, ni les pages de présentation (ASPX) ni les Portlets (ASCX) ne comportent de code applicatif. La seule incursion du langage C# dans ces pages se limite aux éléments de liaison de données (imbriqués dans les balises <%# %>), incontournables dans les listes, mais qui se bornent à afficher les propriétés des objets métier.
La réaction aux événements utilisateur se trouve reléguée dans les CodeBehind respectifs de chaque page ou chaque Portlet. Généralement, réagir à un événement utilisateur revient :
à interagir avec le modèle des objets métier (au travers des contrôleurs de cas d'utilisation),
éventuellement récupérer un ensemble d'objets métier à présenter à l'utilisateur,
puis à décider de la prochaine page ou de la prochaine Portlet à afficher.
Or, dans un portail comme dans de nombreuses applications, il existe souvent de nombreuses manières de déclencher une action. Pour éviter d'avoir à dupliquer le code de réaction à un événement utilisateur, nous avons choisi de mettre en facteur ces Actions dans un ensemble de classes C# que nous avons qualifiées de UserCommands.
Le code des CodeBehind se simplifie donc à l'extrême. Chacun n'a plus qu'à :
- récupérer les données rapatriées par la dernière commande exécutée, et associer ces informations à un composant de présentation qui fera le reste (composant atomique, ou <asp:DataList/>)
- réagir à certains événements utilisateur (sélection d'un élément dans une liste, très souvent) et déclencher la bonne UserCommand.
Les UserCommands, à leur tour, se voient affecter la responsabilité d'invoquer sur les contrôleurs de cas d'utilisation les bonnes actions système, de récupérer les informations éventuellement retournées, et de décider de la prochaine page ou Portlet à afficher.
Derrière les UserCommands, les contrôleurs de cas d'utilisation masquent toute la complexité du déclenchement des scénarii d'utilisation sur le backend de notre application. Ils pilotent à la fois la couche d'accès aux données, et l'invocation de méthodes sur les objets métier.
Les objets métier, comme vous avez pu le noter dans le diagramme de classes précédent, ne sont en réalité que des interfaces. Ce formalisme nous permet, dans la couche de présentation, de ne dépendre d'aucune classe d'implémentation. Nous reviendrons, dans les paragraphes suivants, sur les bienfaits escomptés par cette pratique.
En réalité, les objets métier concrets ont été, tout comme la couche d'accès physique aux données, générés automatiquement par DTM, l'outil de mapping Objet-Relationnel de la société Evaluant. Et ce, à partir du modèle Objet UML bien entendu. Du coup, dans tout le projet, nous n'avons pas du tout eu à gérer cette complexité : pour tout dire,
nous n'avons pas tapé la moindre requête SQL,
nous n'avons effectué aucune tâche administrative sur la base de données (création des tables, remplissage des tables avec des données de test)
et malgré tout nous sommes assez confiants sur les performances globales de l'application puisque DTM gère lui-même un cache objet qui évite les aller-retours systématiques jusqu'à la base de données.
Pour reprendre tous ces éléments, rien ne vaut un petit diagramme d'architecture. Vous allez reconnaître le modèle multi-couche, si souvent présenté à travers nos articles.
|
|
Techniquement, vous ne serez pas surpris par l'arborescence des namespaces du projet (conformément aux conventions Java, nous nous sommes imposé d'organiser répertoires et fichiers exactement comme les namespaces et classes C#, ce qui facilite la compréhension et permet une recherche rapide) :
|
|
Une petite explication s'impose :
Remarque : nous n'avons pas implémenté d'accès à la BLL via .NET Remoting ou les WebServices, mais comme vous allez vite le comprendre, ce serait un jeu d'enfant que d'ajouter cette possibilité. Et cela, sans changer une seule ligne du code existant. En effet, vous connaissez sans doute la règle dite de l'ouvert-fermé : une application orientée objet doit être ouverte aux évolutions (nouvelles fonctionnalités, etc...), mais fermée aux modifications de code (ajouter une fonctionnalité ne doit pas remettre en cause le code existant). Laissons planer ce mystère encore quelques minutes...
Les paragraphes suivants s'attachent à décrire précisément la conception de chaque couche logicielle, ainsi qu'à vous présenter les choix techniques que nous avons été amenés à faire. Logiquement, nous nous proposons de débuter par ce qui est le plus crucial dans notre application : les objets métier.
Les objets métier du PetShopDNG sont très peu nombreux. Le diagramme de classes est rappelé ci-dessous, mais nous vous devons une explication : comment avons-nous découvert le besoin de développer ces classes ? Eh bien nous sommes tout simplement partis des cas d'utilisation, et avons déterminé l'ensemble des classes participantes à chaque cas :
Participent à BrowseCatalog : Category, Product, Item
Participent à Search : Item
Participent à ManageShoppingCart : ShoppingCart, ShoppingCartLine, Item
Participent à ManagePreferences : Account, CreditCard, Category (catégorie préférée d'un utilisateur)
Après la phase d'analyse/conception, nous sommes donc logiquement arrivés au modèle que vous connaissez déjà :
|
|
Pourquoi avoir utilisé des interfaces, et non des classes ? Tout simplement pour respecter l'une de nos règles d'or : le modèle d'objets métier est central dans une application. Il est donc tout à fait légal que les autres couches logiques en dépendent (couche de présentation, couche de services, couche d'accès aux données). Mais ce couplage doit être le plus faible possible : modifier l'implémentation du modèle objet ne doit en aucun cas remettre en cause l'utilisation qu'en font les autres couches.
Or, comme nous allons le voir dans la section suivante, notre modèle objet est implémenté par des classes automatiquement générées par Evaluant DTM. Imaginez que nous découvrions demain des contraintes qui nous obligent pour une raison ou pour une autre à abandonner ce produit : il ne faut en aucun cas que le choix d'une autre technique d'implémentation des objets métier remette en cause le travail réalisé sur la couche de services ou de présentation !
L'utilisation d'interfaces nous offre cette flexibilité. Bien sûr, c'est au prix d'une lourdeur supplémentaire : il faut maintenir en parallèle les interfaces des objets métier...
L'implémentation du PetShopDNG n'aurait pas pu se faire aussi vite sans un outillage intelligent de génération de code. Comme le coeur de l'application consiste à accéder à des informations, à les modifier, et que les objets métier ne renferment que peu de logique, la majeure partie du temps d'implémentation de la couche d'objets métier aurait été consacrée à la couche d'accès aux données physiques que nous stockons en base de données relationnelle.
Nous aurions pu implémenter nous mêmes un ensemble d'objets DAO (Data Access Objects) qui auraient réalisé la liaison entre nos objets métier et l'interface de programmation ADO.NET. Vu le nombre d'objets métier, cela n'aurait pas été très complexe ; mais c'eût été se voiler la face que de croire que l'accès aux données se borne à instancier des objets métier à partir d'informations relationnelles brutes et réciproquement. Comme l'explique Sébastien Ros dans son article sur le mapping Objet / Relationnel, il faut également gérer la cohérence des objets chargés en mémoire, implémenter un cache, la notion de transaction, le chargement tardif, etc ...
La complexité de cette tâche n'est pas insurmontable, mais il ne faut pas la négliger. D'autant que le travail est à répéter pour chaque type d'objet métier. Un générateur de code est donc le bienvenu.
ObjectSpaces, Evaluant DTM, Deklarit, Pragmatier... Quel outil choisir ? Nous avions été séduits par l'approche et l'expertise des personnes d'Evaluant à l'époque où Sami avait écrit un article sur l'outil DTM (Data Tier Modeler). De plus, le fait qu'ObjectSpaces de Microsoft soit aujourd'hui encore en version pré-bêta et que Deklarit ou Pragmatier développent leur propre PetShop nous a conforté dans notre choix.
Le premier risque, en s'appuyant sur un outil, est bien entendu de perdre du temps à le prendre en main. Ce n'est pas le cas avec DTM : il s'installe comme un charme sous forme de plug-in dans VisualStudio.NET, et vous permet, même dans la version d'évaluation, d'avoir jusqu'à 5 classes dans votre modèle.
Il restait donc à saisir dans un langage propriétaire une copie de notre modèle objet ...? Non bien sûr! Vous vous doutez bien que DTM est un outil qui, par excellence, supporte le diagramme de classes standard UML : il suffit de lui fournir un modèle au format XMI (ou Rational Rose) pour qu'il puisse procéder à la génération de la couche DAL (Data Access Layer).
En creusant un peu, on apprend que DTM "triche", et génère en interne un document XML maison qui reflète le modèle de classes à générer. Nous n'avons pas résisté au plaisir de vous montrer à quoi cela ressemble (c'est tellement plus clair qu'un document XMI !):
|
|
Ce document peut, lui aussi, être fourni en entrée à DTM. Cela permet aux personnes peu outillées de saisir le modèle en XML et de faire les premiers tests. Il est certain qu'un éditeur UML graphique simplifie la tâche : nous rappelons aux amateurs que ArgoUML est un éditeur UML écrit en Java, OpenSource, et qu'il produit des documents au format XMI. Il peut dont tout à fait suffire à générer le modèle sur lequel DTM va travailler.
DTM étant un plug-in de VisualStudio.NET, il est complètement intégré au paysage. Cela va même plus loin : la génération de code se fait directement dans le projet en cours ! On ne sort donc même pas de son environnement de développement !
Le code généré est organisé en sous répertoires, dont les principaux sont
DTM\BusinessObjects
Service
Plus précisément, voici la structure de classes produite par DTM :
|
|
Cela n'a l'air de rien, mais nous disposons dorénavant d'une couche d'accès aux données complètement orientée objet, qui gère un cache transactionnel, la pagination, les requêtes sur le modèle objet... sans avoir fait le moindre effort. Mieux, un script de création de la structure de la base de données est généré automatiquement par DTM : on peut d'ailleurs l'exécuter d'un simple click-droit dans l'interface du Plug-In.
Revenons un instant sur les classes générées :
DTM\BusinessObjects : contient l'implémentation de tous les objets métier persistants (Category, Product, Item, Account, CreditCard).
Service : contient un ensemble de factories permettant de piloter l'activité de chargement des objets. Plusieurs techniques sont disponibles, dont la récupération d'un objet en connaissant son identifiant, le lancement d'une requête OPath (requête sur le modèle objet, complètement indépendante du modèle de stockage), ou même une requête SQL (pour les indécrottables, mais bien évidemment, le résultat est une collection d'objets !).
L'utilisation des classes générées est complètement intuitif. Par exemple, itérer sur tous les Product dont la Category a une propriété name de valeur "Bird" revient à exécuter le code suivant :
| foreach (Product p in PetShopDNG.DAL.DtmImpl.Services.Factory.ProductFactory.GetCollectionOPath(string.Format("Product.Category[Name='{0}']", "Bird")); |
Adaptation d'impédance entre les interfaces métier et les objets de la DAL
Le seul hic de notre démarche, c'est que nos bonnes pratiques de conception (séparer clairement par une couche d'interfaces la couche de présentation de celle des objets métier) nous amène à une impasse : DTM génère des objets métier qui héritent de PObject (objet persistant), mais qui n'implémentent aucune interface. Or si le reste de notre système veut survivre à une évolution technologique (typiquement le choix d'un autre outil de mapping Objet / Relationnel), nous devons nous tenir à utiliser des interfaces comme garantie du couplage faible.
Résumons donc les données du problème de conception : d'un côté, nous disposons d'interfaces de nos objets métier. De l'autre, les classes générées par DTM, qui héritent de PObject mais qui n'implémentent aucune interface (et que nous sommes résolus à ne pas modifier).
La solution qui nous a semblé la plus naturelle a été d'utiliser le Design Pattern décorateur pour chaque classe générée par DTM. Le décorateur en question implémente l'interface métier correspondante. Ainsi, nous tirons parti à la fois de la puissance de DTM, et du couplage faible induit par les interfaces.
Du coup, en parallèle des classes Category, Product, Item..., vous trouverez dans le namespace PetShopDNG\DAL\DtmImpl un ensemble de classes logiquement nommées CategoryDecorator, ProductDecorator, ItemDecorator... L'idée est très simple : dès que la couche de présentation demandera à récupérer des objets métier (via la BLL présentée au paragraphe suivant), on lui renverra en réalité pour chaque objet une instance du décorateur associé. Comme les décorateurs implémentent les interfaces ICategory, IProduct, IItem, la couche de présentation ne sera pas vraiment consciente de cette astuce : pour elle, il peut s'agir de n'importe quelle classe à condition que celle-ci implémente l'interface métier correspondante !
Comme dit le vieil adage : "le code C# vaut mieux qu'un long discours", donc voici un exemple de décorateur, pour la classe Category :
namespace
PetShopDNG.DAL.DtmImpl {
public class CategoryDecorator : PetShopDNG.DAL.ICategory { private PetShopDNG.DAL.DtmImpl.Dtm.BusinessObjects.Category category; public PetShopDNG.DAL.DtmImpl.Dtm.BusinessObjects.Category DecoratedCategory{ get{return category;} } public string Id {get{return category.Id;} set{category.Id = value;} } public CategoryDecorator(PetShopDNG.DAL.DtmImpl.Dtm.BusinessObjects.Category category) {this.category = category; } public string Name {get{return category.Name;} set{category.Name = value;} } public string AdvicePhoto {get{return category.AdvicePhoto;} set{category.AdvicePhoto = value;} } public System.Collections.ICollection Products {get{return CollectionsDecorator.DecoratedCollection(category.Products);} } } } |
| PetShopDNG\DAL\DtmImpl\CategoryDecorator.cs |
Vous noterez que nous avons été un peu laxiste concernant les
collections d'objets métier : en toute rigueur, il faudrait également créer des
classes décoratrices pour chaque collection spécialisée d'objets métier. En
première approximation, nous nous sommes contentés d'utiliser une collection
générique (une ArrayList) pouvant contenir n'importe quelle liste
d'objets métier.
Or, cette collection doit porter un ensemble de décorateurs vers nos objets métier pour ne pas nous retrouver face au problème d'adaptation d'impédance (les objets générés par DTM n'implémentent pas nos interfaces métier). Nous avons donc introduit une classe supplémentaire, PetShopDNG.DAL.DtmImpl.DecoratedCollection, qui est tout simplement une factory de listes de décorateurs. Son rôle est d'instancier des collections de décorateurs, dont le type dépend de la collection spécialisée qu'on lui fournit en paramètre. Par exemple, la méthode CollectionsDecorator.DecoratedCollection(CategoryCollection cc) produira une collection de CategoryDecorator.
Par curiosité, jetons un oeil à cette factory de collections décorées :
namespace
PetShopDNG.DAL.DtmImpl {
using System.Collections; using PetShopDNG.DAL.DtmImpl.Dtm.BusinessObjects; public class CollectionsDecorator{ public static ICollection DecoratedCollection(CategoryCollection dtmColl){ new ArrayList(); foreach (Category c in dtmColl){ new CategoryDecorator(c)); } return list;} public static ICollection DecoratedCollection(ProductCollection dtmColl){new ArrayList(); foreach (Product p in dtmColl){ new ProductDecorator(p)); } return list;} public static ICollection DecoratedCollection(ItemCollection dtmColl){new ArrayList(); foreach (Item i in dtmColl){ new ItemDecorator(i)); } return list;} } } |
| PetShopDNG\DAL\DtmImpl\CollectionsDecorator.cs |
Conformément aux bonnes pratiques usuelles, nous associons un contrôleur à chaque cas d'utilisation. Il aura la responsabilité de piloter les différents scénarii du cas d'utilisation. Par exemple, le contrôleur du cas "Search" est responsable de rechercher les catégories de produits par nom et par identifiant, de rechercher les produits, les items, etc...
Ces contrôleurs sont les véritables façades fonctionnelles du système : ils centralisent et simplifient l'accès aux règles métier. Dans le PetShopDNG, nous n'avons implémenté que 3 contrôleurs :
Contrôleur d'authentification
Contrôleur de recherche
Contrôleur de gestion du caddie virtuel
Pour vous en présenter les méthodes, rien de vaut une copie d'écran de la "Class View" de VisualStudio.NET :
|
|
Vous trouverez enfin un quatrième contrôleur : celui des tests. Il ne nous sert qu'à peupler le système de données de test, de manière à ce que l'on puisse parcourir le site et visualiser des objets significatifs. Ce contrôleur de tests pourra probablement devenir un contrôleur d'importation de données, car on peut tout à fait imaginer que les catégories, produits et items existent déjà dans un autre système, auquel cas il faudra prévoir la migration des données existantes.
Vous l'aurez probablement remarqué : les contrôleurs de cas d'utilisation ne sont en réalité que des interfaces. En effet, si les fonctionnalités métier doivent apparaître explicitement dans nos contrôleurs, la technologie d'implémentation, elle, doit être masquée. Nous ne devons donc en aucun cas créer une dépendance entre la couche de présentation et une classe qui dépendrait, dans notre implémentation actuelle, de DTM.
La couche de présentation ne doit donc pas instancier elle-même ces contrôleurs : il faut déléguer cette responsabilité à une abstract factory, c'est-à-dire une classe chargée d'instancier les contrôleurs sous-jacents. Cette factory sera malheureusement dépendante des classes techniques (générées par DTM dans notre implémentation) : il en existera donc une par implémentation de la couche d'accès aux données (DAL). Pour vous faire une idée, considérons ensemble le code de la factory actuelle, PetShopDNG.BLL.DtmImpl.DtmAbstractFactory :
namespace
PetShopDNG.BLL.DtmImpl {
} |
| PetShopDNG\BLL\DtmImpl\DtmAbstractFactory.cs |
La couche de présentation est bien obligée de connaître le nom de notre Factory pour pouvoir instancier les bons contrôleurs de cas d'utilisation. Or, nous avons été terriblement gênés par cette dépendance résiduelle. En effet, pour changer d'implémentation de la couche d'accès aux données (DAL) et de couche de services (BLL), il nous faudra modifier la couche de présentation.
Inconcevable! Ce couplage fort devait disparaître.
Prenez une minute pour imaginer une solution élégante... Vous avez trouvé ? Nous sommes probablement arrivés aux mêmes conclusions. Le paragraphe suivant nous les présente.
Eliminer le couplage entre présentation et couche de services ? Facile : il suffit que l'abstract factory elle-même se cache derrière une interface. La couche de présentation ne référencerait que cette interface, et invoquerait ses méthodes pour instancier les contrôleurs concrets, sans savoir quelle classe concrète elle manipulerait.
Toutefois, le problème de l'instanciation reste entier : comment instancier la bonne classe d'implémentation sans la référencer dans le code ? Il faudrait passer par la réflexion .NET ! Mais comment installer du code dans une interface ? C'est impossible.
Nous n'avons pas le choix : au lieu d'une interface, utilisons une classe abstraite (PetShopDNG.BLL.AbstractFactory) :
|
|
Les méthodes de construction des contrôleurs doivent rester abstraites, mais lors du chargement de la classe, nous pouvons installer quelques lignes qui instancient une factory concrète par réflexion. Voici le code de notre classe générique :
namespace PetShopDNG.BLL {
} |
| PetShopDNG\BLL\AbstractFactory.cs |
Le nom de la classe concrète (qui hérite de l'AbstractFactory
abstraite) est externalisé dans le fichier de configuration générique
Web.config) :
|
<?xml
version="1.0"
encoding="utf-8"
?> < configuration>
</appSettings> <system.web>
<sessionState mode="InProc" stateConnectionString="tcpip=127.0.0.1:42424" sqlConnectionString="data source=127.0.0.1;user id=sa;password=" cookieless="false" timeout="20"/> <globalization requestEncoding="utf-8" responseEncoding="utf-8"/> </system.web> </configuration> |
| Web.config |
Grâce à cette conception, nous avons éliminé tout couplage entre les couches utilisatrices (ici : la couche de présentation Web, qui est décrite plus bas) et les couches DAL et BLL. Si demain nous avions à ré-implémenter la couche d'accès aux données, il suffirait de :
développer de nouvelles classes qui implémentent les interfaces métier
développer de nouveaux contrôleurs de cas d'utilisation qui implémentent les interfaces ISearchController, IShoppingController et IAuthenticationController
développer une classe XXXAbstractFactory qui hérite de PetShopDNG.BLL.AbstractFactory et qui instancie les classes des contrôleurs de cas d'utilisation
référencer le nom de cette classe (XXXAbstractFactory) dans le fichier de configuration Web.config
Le coeur de l'application est développé. Les contrôleurs de cas d'utilisation nous en masquent la complexité, et la couche DAL permet de rendre les objets métier persistants. Il ne reste plus qu'à présenter ces services de haut niveau à l'utilisateur, sous la forme de pages Web.
Nous vous proposons un petit voyage dans l'application PetShopDNG, "comme si vous y étiez", en vous donnant les explications concernant la conception de la couche de présentation au fur et à mesure. Rappelez-vous simplement d'une chose : nous nous sommes interdits, dans les pages de présentation, de placer la moindre ligne de code logique. Tout est relégué dans les CodeBehind, ou dans les UserCommands qui factorisent les actions récurrentes un peu à la manière du célèbre Framework MVC Java : Struts.
|
Le point d'entrée dans l'application est la page Index.aspx. Elle ne fait que présenter le texte de bienvenue, et offrir deux moyens de pénétrer dans l'application :
Cliquer sur l'un des liens nous mène directement dans le CodeBehind, qui ne fait que nous rediriger vers la page suivante (Template.aspx). Dans le cas où vous cliqueriez sur le lien de remise à niveau de la base de données, ce serait le contrôleur de test qui serait invoqué (rappelez-vous : c'est lui qui porte la responsabilité de peupler la base de données automatiquement). |
![]() |
| La page principale (Template.aspx) se compose d'un ensemble de contrôles utilisateur que l'on peut qualifier de Portlets dans ce contexte. Certains sont systématiquement visibles :
D'autres Portlets ne sont activés qu'à la demande. Et par défaut, celle qui s'affiche présente les catégories d'animaux en mode graphique (image cliquable) : il s'agit de Body.ascx. Cliquons sur la catégorie "Fish" (à gauche) ou sur l'image du poisson (au milieu) : cela déclenche dans le CodeBehind l'exécution de la commande ShowProductsByCategoryCommand. Celle-ci lance à son tour le contrôleur du cas d'utilisation Search, qui lui renvoie une collection d'objets de type IProduct. La commande mémorise cette liste, et décide de la prochaine page à afficher, ou plutôt de la prochaine Portlet à afficher : Category.ascx. |
![]() |
| Le portail se reconstruit; toutes les Portlets statiques restent présentes, mais au centre de l'écran, la Portlet Body.ascx a été remplacée par Category.ascx. Elle nous présente la liste des produits disponibles dans la catégorie des poissons. Sans surprise, Category.ascx s'appuie pour cela sur le WebForm <asp:DataList/>, qu'il suffit de lier avec ... la liste de IProduct mémorisé par la commande précédente (ShowProductsByCategoryCommand). Le code de notre Portlet, comme toutes les autres, limite l'utilisation
du langage C# aux expressions de DataBinding du type Cliquons sur l'un des produits, par exemple "AngelFish", et la commande ShowItemsByProductCommand s'exécute à son tour. |
![]() |
| ShowItemsByProductCommand récupère elle aussi une liste d'objets métier via le contrôleur du cas d'utilisation Search. Ces objets, de type IItem, sont mémorisés par la commande puis présentés par la Portlet Product.ascx. Ce nouveau contrôle nous permet :
Cliquons sur l'un des articles, par exemple "Spottless AngelFish". Cela déclenche la commande ShowItemCommand. |
![]() |
| ShowItemCommand récupère l'objet IItem correspondant à la sélection, et le fait présenter par la Portlet Item.ascx. A nouveau, nous pouvons ajouter cet article au caddie virtuel. On voit donc poindre l'intérêt de notre approche par "Commandes" : si nous avions implémenté directement dans les CodeBehind le déclenchement des contrôleurs de cas d'utilisation et la navigation vers la bonnes Portlet de présentation, le code aurait été dupliqué autant de fois qu'il y a de manières différentes de lancer la même action sur notre système. Les commandes ajoutent donc une couche supplémentaire à notre architecture, mais garantissent la cohérence des actions utilisateur sur le système, quelle que soit la métaphore de présentation utilisée (un lien, une image, un formulaire, ou un lien qui apparaît à plusieurs endroits).
En cliquant sur "Add to cart", nous déclenchons la commande AddItemToCartCommand. |
![]() |
| Malheureusement, nous ne sommes pas authentifiés. Donc notre caddie virtuel n'est pas encore initialisé. Si c'est notre première connexion au site, nous ne disposons pas encore d'identifiant : il nous faut donc passer par une phase d'authentification ("New user"). Sinon, "Returning user" nous permet de saisir identifiant et mot de passe. Cliquons sur ce lien... |
![]() |
| Qui nous amène à la Portlet d'authentification ci-contre. Pour vous faciliter la tâche, nous avons créé un utilisateur de test. Son identifiant est "tom", et son mot de passe "password". Ce formulaire nécessite quelques validations :
C'est l'occasion rêvée de tirer parti des contrôles de validation de surface préconçus dans les WebForms : ShowItemsByProductCommand et CompareValidator. Si la validation de surface aboutit, le bouton "Create New Account" déclenche la commande TryToAuthenticateCommand, qui interagit à son tour avec le contrôleur du cas d'utilisation Authentication. Bien sûr, si nous nous trompons de mot de passe, ou si l'identifiant n'existe pas, le contrôleur (et donc la commande) nous renvoie une exception métier. Dans la Portlet d'authentification, il y a donc quelques zones d'erreur : invisibles par défaut, elles ne sont activées que si une exception survient. |
![]() |
| Si l'authentification réussit, l'utilisateur se retrouve à la page d'accueil. Il est authentifié, et aura donc accès à son caddie virtuel. Que l'on clique sur le lien rémanent en haut à droite ou que l'on ajoute un article au caddie virtuel, on est toujours amené à prendre connaissance du contenu actuel du caddie virtuel. A nouveau, cette Portlet propose plusieurs actions :
|
![]() |
| Un petit moteur de recherche rapide se cache en haut à droite du PetShopDNG. Il suffit d'y entrer quelques lettres à rechercher dans les noms des articles et de cliquer sur Search pour afficher la liste des articles dans la Portlet dynamique Product.ascx. Deux remarques :
|
![]() |
| Dernière fonctionnalité du site : une fois authentifié, l'utilisateur a accès à son compte personnel. On y retrouve la civilité, les informations relatives aux moyens de paiement (carte de crédit), ainsi que quelques préférences. En bas de page (qui n'apparaît pas ci-contre), on trouve un lien permettant de basculer du mode "visualisation" en mode "édition" du compte utilisateur. |
![]() |
| Le mode "édition" respecte l'ordre des informations utilisé dans la page de "visualisation" du compte. Tout est devenu modifiable, et en fonction des types de données, nous avons utilisé soit des zones de saisie de teste, des listes déroulantes, ou encore des cases à cocher. A nouveau, sachez qu'il existe deux préférences utilisateur disponibles dans cette page :
|
![]() |
| Un utilisateur ayant activé les deux préférences dont nous venons d'évoquer l'existence aurait un portail personnalisé identique à la copie d'écran ci-contre. |
![]() |
La conception et le développement de la couche de présentation ont été les étapes qui ont soulevé le plus de questions. Nous voulions arriver à une solution très simple (très proche de ce que connait un développeur ASP.NET typique) et tirer parti du mécanisme des événements ASP.NET. En même temps, nous souhaitions implémenter un véritable portail, et non une succession de pages incluant systématiquement des contrôles ASCX (ce qui s'avère difficile à maintenir).
Le plus gros problème auquel nous avons été confronté est celui du rapport existant entre le modèle des événements, le ViewState et le chargement dynamique des contrôles. En effet, les contrôles que l'on instancie dynamiquement par la méthode Page.LoadControl() ne sont pas sauvegardés dans le ViewState par défaut. Donc si l'on clique sur l'un de ces contrôles, l'événement correspondant a pour cible... un élément de l'arborescence de la page qui n'existe plus en mémoire.
Nous avons donc le choix :
soit nous mémorisons l'état des contrôles chargés dynamiquement dans une variable de session, ce qui permet de replacer les contrôles dans leur état précédent lorsque l'utilisateur se reconnecte
soit nous sérialisons manuellement ces mêmes contrôles dans le ViewState (mais les contrôles ne sont pas sérialisables par défaut)
soit nous ré-exécutons la commande qui nous revoie les informations permettant de replacer le contrôle dans son état précédent (par exemple : pour un clic sur une catégorie, il suffirait de recharger le contrôle avec la liste des catégories du système pour pouvoir déclencher l'événement clic correspondant).
soit enfin, nous créons systématiquement tous les contrôles (nous nous débarrassons du chargement dynamique des contrôles ASCX), mais nous n'en affichons qu'un à la fois.
Ne souhaitant pas implémenter un Framework complexe au-dessus de ASP.NET, nous avons immédiatement écarté les deux premières solutions. La troisième, quant à elle posait deux problèmes :
elle multipliait par deux le nombre de requêtes de récupération des objets métier. Dans un système classique, cela dégraderait les performances dans des proportions inacceptables. Heureusement, dans notre architecture, le cache objet éviterait une partie du désastre. Mais si nous remplaçons, plus tard, les couches BLL et DAL par une implémentation moins efficace (un accès distant à la BLL typiquement, en utilisant .NET remoting), nous nous exposons à de sérieuses conséquences.
le second problème est plus grave : exécuter deux fois la même requête à des moments différents risque de ne pas nous retourner les mêmes résultats. Si de nouveaux articles ou catégories apparaissent dans notre système, le clic sur le deuxième article d'une liste risque de ne pas avoir l'effet escompté (il sélectionnerait le second article du nouveau résultat de la requête précédente) !
Pour faire simple, nous n'avions donc pas le choix : nous avons retenu la quatrième option. Tout chargement dynamique de contrôle ASCX a donc été exclu du PetShopDNG. Chaque page instancie donc tous les contrôles existants; certains sont systématiquement visibles, d'autres ne seront activés qu'à la demande. Typiquement, parmi les contrôles qui peuvent apparaître au centre du portail, un seul peut être visible à la fois.
Du coup, comme les contrôles sont statiques, le mécanisme des événements fonctionne très bien.
Le risque de notre solution est toutefois que le surcoût de création systématique de l'ensemble des contrôles ASCX soit préjudiciable. Nous avons donc procédé à deux "optimisations" :
lorsqu'un contrôle est "désactivé", c'est-à-dire lorsqu'il devient invisible, on lui demande également de ne pas s'enregistrer dans le ViewState. De cette manière, seul le contrôle actif place son état dans le ViewState ce qui évite une surcharge des volumes de données échangées entre client et serveur Web.
les contrôles ne se lient pas aux données (DataBind) lors de l'événement habituel "PageLoad", mais redéfinissent la méthode DataBind(). Cela permet de ne lier que le contrôle actif, à chaque requête.
Cette intelligence d'activation / désactivation des contrôles est du ressort du contrôle PortalContent.ascx, qui contient la définition de tous les contrôles dynamiques. Voici son CodeBehind, qui explique mieux que les mots son comportement :
namespace
PetShopDNG.UserControls {public abstract class PortalContent : System.Web.UI.UserControl { protected override void OnPreRender(System.EventArgs e){ string viewName = null) ? cmd.NextViewToDisplay : PetShopDNG.UserCommands.WebViews.INIT; // Make the right control visible foreach (System.Web.UI.Control c in Controls){ if (c.Visible) c.DataBind(); else c.EnableViewState = false; } base.OnPreRender(e);} } } |
| PetShopDNG\UserControls\PortalContent.cs |
Juste une remarque : la ligne la moins explicite dans ce code
est celle qui récupère la commande venant d'être exécutée :
| PetShopDNG.UserCommands.WebLocalSingleton.GetInstance(Context).CurrentCommand; |
C'est en effet la responsabilité de chaque commande que de savoir quelle Portlet doit être affichée. Notez que lors de la première connexion, une Portlet par défaut doit être positionnée. Dans notre cas, il s'agit de WebViews.INIT, qui correspond à la portlet Body.ascx (souvenez-vous : c'est l'image cliquable contenant les différentes photos de catégories d'animaux).
Au fil des discussions sur ce sujet, il semblerait qu'en termes de performances pures, le surcoût induit par l'instanciation d'autant de contrôles ne soit pas forcément très important, comparé au chargement dynamique de contrôles par la méthode Page.LoadControl(). En effet, cette dernière utilise des techniques d'analyse, de chargement et d'instanciation dynamiques qui alourdissent la construction d'un simple contrôle.
Nous n'avons réalisé aucun test de performances. Mais à l'usage, il ne semblerait pas que notre choix d'implémentation porte à conséquence. Vous qui allez tester le PetShopDNG, n'hésitez pas à nous faire part de vos expériences à l'usage !
Installer le PetShopDNG est un trivial :
téléchargez et exécutez le setup MSI d'installation automatique, qui vous demandera de choisir un nom de répertoire virtuel pour installer le PetShopDNG
créez, dans votre base de données SQLServer2000 locale, une base appelée "PSDNG". Laissez-la vide, le PetShopDNG s'occupe de tout.
connectez-vous à l'adresse http://localhost/VotreRepertoireVirtuel/Index.aspx
amusez-vous bien !
Le PetShopDNG respecte les bonnes pratiques d'architecture et de conception communément admises sur la plate-forme .NET :
une couche de présentation (ASPX, ASCX) ne contenant aucune logique applicative
la réaction aux événements utilisateur se fait dans les CodeBehind des pages ou des contrôles utilisateur
plusieurs événements pouvant déclencher les mêmes actions (et donc aboutir au même résultat), nous avons introduit une couche de commandes réutilisables, qui sont sollicitées par les CodeBehind
CodeBehind ou commandes passent systématiquement par une Abstract Factory pour instancier un contrôleur de cas d'utilisation
les contrôleurs de cas d'utilisation font partie de la couche dite "BLL" (Business Logic Layer), et pilotent les différents scénarii des cas d'utilisation correspondants. Ces BLL sont interchangeables et le client n'a jamais de visibilité sur leur implémentation, seule l'interface fait foi
ces mêmes contrôleurs s'appuient sur une couche d'accès aux données (DAL : Data Access Layer) pour récupérer les informations brutes dans un support de stockage (ici une base de données relationnelle) et réaliser le mapping Objet / Relationnel
L'utilisation d'interfaces (et de factories) à tous les niveaux nous garantit un couplage faible entre les couches, et une grande évolutivité : par exemple, imaginer une architecture où la BLL serait sollicitée à distance via .NET remoting et où les objets métier seraient passés par valeur de la BLL à la couche de présentation ne pose aucun problème. Mieux : il n'y aurait aucune ligne de code à modifier dans la couche de présentation, puisque même le nom de la fabrique à utiliser pour instancier les contrôleurs de cas d'utilisation est paramétré par le fichire Web.config.
Cela va sans dire, si l'on peut imaginer cette architecture à base de .NET Remoting, on peut faire le même travail sur un socle de WebServices, ou encore sur les ServicedComponents. Nous vous laissons y méditer...
Nous voilà donc avec une architecture flexible et un bel exemple de conception / développement d'un site Web de bout en bout. Nous espérons que cette expérience, relatée à travers cet article, ainsi que le code source du PetShopDNG, vous seront profitables. Téléchargez le code sans complexe, et n'hésitez surtout pas à nous faire part de vos remarques et de vos critiques constructives : toute architecture est perfectible...
Auteur : Thomas GIL
Copyright © Janvier 2003
Licence GPL
Tous fichiers sont fournis sous la licence GPL.