Présentation du PetShop DotNetGuru par Thomas GIL (thomas.gil@valtech.fr)

Introduction

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 :

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 :

Au contraire, nous souhaitions éviter de :

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.

Phase 01 : Recueil des besoins

Les besoins fonctionnels

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 :

Figure A : Diagramme de cas d'utilisation

 

Scénarii imagés

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) :

D'autres Portlets seront affichées dynamiquement, en fonction des demandes de l'utilisateur :

Enfin, deux Portlets dépendent des préférences de l'utilisateur (stockées dans son compte) :

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 :

  1. [Optionnel] Authentification de l'utilisateur : passage obligé pour accéder au caddie virtuel (dans une Portlet dynamique)

  2. Sélection d'une catégorie (dans la Portlet statique correspondante)

  3. Sélection d'un produit (Portlet dynamique)

  4. Sélection d'un article (Portlet dynamique)

  5. 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.

Phase 02 : Analyse rapide

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 :

Figure C : Diagramme de classes - objets métier du PetShopDNG

Phase 03 : Architecture technique du PetShopDNG

Architecture globale du PetShopDNG

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 :

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'à :

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,

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.

Figure D : Diagramme d'architecture du PetShopDNG

Organisation des différentes couches logicielles

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) :

Figure E : Organisation du code du PetShopDNG

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.

Phase 04 : La couche d'accès aux données (DAL)

Le modèle des 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 :

Après la phase d'analyse/conception, nous sommes donc logiquement arrivés au modèle que vous connaissez déjà :

Rappel Figure C : Diagramme de classes - objets métier du PetShopDNG

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...

La couche d'accès à la base de données

Du besoin d'un outil de Mapping O/R

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.

Démarche projet avec DTM (Data Tier Modeler) de la société Evaluant

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 !):

Figure F : Modèle XML des objets métier à rendre persistants grâce à Evaluant DTM

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.

Que génère DTM ?

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

Plus précisément, voici la structure de classes produite par DTM :

Figure G : Organisation des classes générées par Evaluant 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 :

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){

IList list = new ArrayList();

foreach (Category c in dtmColl){

list.Add(new CategoryDecorator(c));

}

return list;

}

public static ICollection DecoratedCollection(ProductCollection dtmColl){

IList list = new ArrayList();

foreach (Product p in dtmColl){

list.Add(new ProductDecorator(p));

}

return list;

}

public static ICollection DecoratedCollection(ItemCollection dtmColl){

IList list = new ArrayList();

foreach (Item i in dtmColl){

list.Add(new ItemDecorator(i));

}

return list;

}

}

}

PetShopDNG\DAL\DtmImpl\CollectionsDecorator.cs

 

Phase 05 : La couche de services (BLL)

Les contrôleurs de cas d'utilisation

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 :

Pour vous en présenter les méthodes, rien de vaut une copie d'écran de la "Class View" de VisualStudio.NET :

Figure H : Interfaces des contrôleurs de cas d'utilisation (couche BLL)

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.

Le pattern AbstractFactory

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 {

public class DtmAbstractFactory : PetShopDNG.BLL.AbstractFactory {

private IAuthenticationController authenticationCtrl = new AuthenticationController();

private ISearchController searchCtrl = new SearchController();

private IShoppingController shoppingCtrl = new ShoppingController();

private ITestController testCtrl = new TestController();

static DtmAbstractFactory(){

PetShopDNG.DAL.DtmImpl.Dtm.Utils.DataBase.GetInstance().SetDefaultConnection

(System.Configuration.ConfigurationSettings.AppSettings["dsn"]);

}

public override IAuthenticationController AuthenticationController {

get{ return authenticationCtrl; }

}

public override ISearchController SearchController {

get{ return searchCtrl; }

}

public override IShoppingController ShoppingController{

get{ return shoppingCtrl; }

}

public override ITestController TestController{

get{ return testCtrl; }

}

}

}

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.

Solution technique : interfaces, classes abstraites et réflexion

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) :

Figure I : AbstractFactory (couche BLL)

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 {

public abstract class AbstractFactory {

private static AbstractFactory fact;

static AbstractFactory(){

string factoryClassName =

System.Configuration.ConfigurationSettings.AppSettings

["abstractFactory"];

fact = (AbstractFactory)

System.Reflection.Assembly.GetExecutingAssembly().

CreateInstance(factoryClassName);

}

public static AbstractFactory Factory {

    get{return fact;}

}

public abstract IAuthenticationController AuthenticationController {get;}

public abstract ISearchController SearchController {get;}

public abstract IShoppingController ShoppingController {get;}

public abstract ITestController TestController {get;}

}

}

 
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>

<add key="dsn" value="server=localhost;database=PSDNG;UID=sa;PWD=" />

<add key="abstractFactory" value="PetShopDNG.BLL.DtmImpl.DtmAbstractFactory" />

</appSettings>

<system.web>

<compilation defaultLanguage="c#" debug="false"/>

<customErrors mode="Off"/>

<authentication mode="None" />

<trace enabled="false" requestLimit="10" pageOutput="true"

traceMode="SortByTime" localOnly="true"/>

<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 :

Phase 06 : La couche de présentation

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.

Au coeur du PetShopDNG

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 :

  • soit en entrant directement dans la boutique en ligne ("Enter the store")
  • soit en entrant dans la boutique après avoir créé la structure de la base de données et avoir peuplé la base de données de test. Si le PetShopDNG vient d'être installé, il faudra passer au moins une fois par ce lien, sans quoi la base de données serait complètement vide.

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 :

  • Le logo en haut à gauche
  • Le moteur de recherche rapide en haut à droite
  • Les liens d'accès immédiat au compte utilisateur, à son caddie virtuel, et à l'authentification (ou à la déconnection, selon le contexte)
  • La liste des catégories d'animaux disponibles
  • Le pied de page (statique)

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
<%# ((IProduct) Container.DataItem).Name %>

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 :

  • soit d'avoir accès au détail de l'article (dont la photo de l'animal)

  • soit d'ajouter directement cet article dans notre caddie virtuel.

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 :

  • chaque zone de saisie doit être renseignée

  • le mot de passe et sa confirmation doivent concorder

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 :

  • Accéder au détail de chaque article du caddie
  • Supprimer une ligne du caddie
  • Modifier les quantités d'articles pour chaque ligne du caddie

 

 

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 :

  • nous n'avons pas implémenté la pagination dans cette liste d'articles qui sera l'objet du prochain article 
  • nous aurions dû créer une Portlet différente pour présenter les résultats de cette recherche, car le titre est peu approprié ("Items for this Product"). Mais notre objectif est ici plus technique que cosmétique... une manière détournée pour dire que nous avons été un peu fainéants...
 

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 :

  • Souhaitez-vous activer la fonctionnalité "Ma liste préférée" ? Auquel cas vous verrez apparaître, sur chaque page du portail, une Portlet supplémentaire qui présente les produits de votre catégorie d'animaux préférée
  • Souhaitez-vous activer la fonctionnalité "Montrez-moi ma catégorie préférée" ? Dans ce cas, une deuxième Portlet apparaîtra pour présenter l'icône de votre catégorie préférée.
 

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.

 

Problèmes techniques rencontrés, solutions retenues

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 :

  1. 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

  2. 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)

  3. 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).

  4. 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 :

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.

Optimisation

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" :

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){

PetShopDNG.UserCommands.IWebCommand cmd =

PetShopDNG.UserCommands.WebLocalSingleton.GetInstance(Context).CurrentCommand;

string viewName =

(cmd != null) ? cmd.NextViewToDisplay : PetShopDNG.UserCommands.WebViews.INIT;

// Make the right control visible

foreach (System.Web.UI.Control c in Controls){

c.Visible = (c.ID == viewName);

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

Performances induites par nos choix de programmation

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 !

Testez le PetShopDNG

Installer le PetShopDNG est un trivial :

Conclusion

Le PetShopDNG respecte les bonnes pratiques d'architecture et de conception communément admises sur la plate-forme .NET :

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