Le PetShop .NET : un anti-pattern d'architecture  par Sami Jaber (jaber@ifrance.com)

Introduction        

Depuis l'annonce du désormais célèbre Benchmark du site TheServerSide opposant .NET à J2EE, il nous est apparu plus que jamais nécessaire d'apporter des éléments de réponse à la communauté .NET quant aux concepts liés aux architectures multi-tiers. Le Java PetStore est une application dont le but a toujours été de mettre en avant ces notions de séparation de couches et de modularité à travers une implémentation technique entièrement basée sur les Enterprise JavaBeans. Si la popularité du PetStore est plus liée à son architecture globale qu'à ses capacités à monter en charge, elle constitue sans aucun doute un modèle de référence pour l'implémentation d'applications en général.

Dans cette optique, l'avènement du PetShop de Microsoft nous a semblé intéressante à plusieurs titres. Tout d'abord, c'est la première fois que la notion d'Architecture avec .NET est autant mise en avant à travers une application de référence, aussi minimaliste soit-elle. Deuxièmement, avec le PetShop, DotNetGuru tenait enfin un exemple concret d'application basée sur les Design Patterns les plus couramment utilisés, qui plus est en C#.

Malheureusement, nous sommes contraint de vous avouer que l'enthousiasme qu'était le notre au départ a vite laissé place à une profonde désillusion lorsque nous avons parcouru la totalité des sources de l'application. Alors que le PetShop est censé représenter un modèle de conception, nous avons été surpris de découvrir que la plupart des concepts élémentaires d'architecture avaient été bafoués ou simplement ignorés. Non seulement la séparation des couches n'est pas respectée mais l'accès aux données, brique importante dans une application multi-tiers a été totalement mise à l'écart au profits d'objets hybrides. 

Vu la tournure des évènements, plusieurs choix s'offraient donc à nous. Soit nous décidions simplement de passer sous silence cette implémentation du PetShop en arguant que son objectif initial était essentiellement la montée en charge et non l'architecture (ce qui n'est pas la réalité), soit nous adoptions une démarche critique mais constructive visant à proposer un article détaillé sur les dangers d'une mauvaise conception tout en apportant des éléments de réponse.
 
La deuxième option nous a semblé la plus judicieuse car cette situation nous permettait par la même occasion de vous illustrer notre vision de l'architecture .NET et avec elle la notion d'anti-pattern d'architecture illustrée par cette implémentation de Microsoft.

Il est très important de comprendre que l'idée initiale à travers cet article n'est pas de rajouter au débat stérile que se livre actuellement partisans J2EE et .NET mais bel et bien de vous illustrer un certain nombre de concepts qui sont chers à DotNetGuru et dont nous vous faisons régulièrement l'écho sur ce site. C'est pourquoi nous nous garderons de tirer de quelconques conclusions hasardeuse en l'absence des éléments concernant le BenchMark de TMC.

Après une brève introduction sur l'architecture multi-tiers, nous vous présenterons l'application PetShop suivi d'une liste de carences en terme de conception et d'Architecture. Pour chaque notion, nous vous apporterons des éléments de réponse à travers des illustrations concrètes en terme de code.

L'architecture du Java PetStore

Pour ceux ne connaissant pas encore le Java PetStore, nous vous invitons à parcourir ses sources. Le PetStore fait appel à plusieurs composants distribués EJB Session dont le rôle est d'interagir avec la couche de données implémentées à l'aide de composants EJB Entités. La séparation des couches est assurée à l'aide des API J2EE et l'interface graphique est constituée autour d'un Framework bâtie sur la base des Blueprints ou "Best Practice" élaborés par Sun. Le schéma suivant illustre l'architecture générale du PetStore :

Copyright Sun Microsystems

L'intérêt d'une telle architecture est avant tout de séparer les couches entre elles. Dans le PetStore, chaque composant distribué situés dans la couche de service implémente une logique métier bien particulière. Plus précisément, un EJB Session correspond à un contrôleur de Use Case UML. Prenons l'exemple du module Catalog, il contient l'ensemble des opérations liées aux produits, catégories ou items. L'objectif de cette approche est la réutilisation mais également l'encapsulation de la couche de données. En effet, un client désirant récupérer la liste des produits d'une catégorie donnée s'adresse à la couche de service qui fait appel à la couche de données chargée de masquer totalement les opérations de lecture et écriture en base. Ainsi en cas de modification de la structure ou du type des données, l'impact sur le client est nul. Enfin, il est à noter que la couche de service doit être totalement indépendante des données manipulées. C'est pourquoi il est strictement interdit de faire appel à de quelconques API de données (JDBC, ...) à l'intérieur de ces objets. Le PetStore respecte entièrement ces règles. Chaque EJB Session fait appel à un EJB Entity totalement indépendant de la structure physique de la base qui délègue les opérations de CRUD (Creation Retrieve Update Delete) à des DAO dans le cas d'EJB BMP ou au Framework dans le cas d'EJB CMP (Container Managed Persistance).

La figure suivante illustre l'ensemble des fichiers sources nécessaires à l'élaboration des concepts précédents :

Le module Order représente le cas idéal pour illustrer le principe précédent. Le composant OrderEJB est un EJB Entité faisant appel à la classe OrderDAO pour assurer sa persistance. Le Design Pattern DAO est composé de 3 objets : l'interface, la fabrique ou Factory et l'implémentation. Le module contient également un package model contenant l'ensemble du modèle objet implémenté par le traitement en question. Ce modèle objet permet d'échanger des données par valeur entre la couche de présentation et la couche de service. Le Design Pattern utilisé ici est le Value-Object.

Pour résumer le principe général, lors de la conception d'une application 3 tiers, il convient de séparer attentivement l'ensemble des couches afin de minimiser les impacts lors d'évolutions futures. Observez le module précédent, s'il s'avère nécessaire de modifier les requêtes SQL, seul l'objet OrderDAOImpl.java nécessitera une mise à jour. Si l'application implique un déploiement dans n'importe quel type de base de données, il suffit simplement de fournir une implémentation adéquate de la classe OrderDAOImpl.java (OrderDAO_Oracle.java ou OrderDAO_SQLServer.java).

Maintenant que vous avez eu un bref aperçu du PetStore, voyons comment a été conçu le PetShop.

Vue d'ensemble des classes du PetShop

La première chose qui attire l'attention lorsque qu'on parcourt les sources du PetShop est le faible nombre de classes. Seulement 10 classes sont présentes pour implémenter toute la logique applicative constituée de l'accès aux données et de la couche de service. Dans l'exemple précédent du PetStore, nous vous avions illustré un module contenant à lui seul plus d'une vingtaine de classes (!). Le langage C# n'étant pas moins verbeux que Java au point de réduire d'un facteur de 10 la taille du code, il est d'ores et déjà évident que nous allons être confronté à un sérieux problème de conception avec le PetShop. Nous y reviendrons.

La couche de présentation constituant "l'emballage" d'une application et la moins stratégique, cet article occultera quelque peu la partie Web du PetShop sur laquelle les griefs sont moindres.

 

La couche de service

La première difficulté face à une application dans laquelle tous les fichiers sont à "plats" à l'intérieur d'un seul et même projet consiste à identifier les classes de la couche de service. Nous avons été contraint de parcourir une à une chaque classe du projet afin de réaliser cette opération de tri. Au passage, avec le Java PetStore, une simple recherche des composants EJB Session ou du package correspondant à chaque module (UseCase) aurait suffit. Le composant Product est le premier de la liste. A priori, il devrait contenir l'ensemble des traitement relatifs aux produits. La première remarque concerne la conception générale. L'objet Product du PetShop .NET n'a aucun équivalent dans le Java PetStore.

Pour le PetStore, un produit est avant tout une entité persistante et n'est pas géré directement en tant que tel. C'est le rôle du contrôleur de UseCase Catalog de renvoyer des produits représentés par un EJB Entité. Voici l'implémentation de l'objet CatalogEJB.java.

public class CatalogEJB implements SessionBean {

    private CategoryROEntityHome catgyHome = null;
    private ProductROEntityHome prodHome = null;
    private ItemROEntityHome itemHome = null; 

    public Category getCategory(String categoryId, Locale locale) {
        Category cat = null;
            CategoryROEntity catEJB = catgyHome.findByPrimaryKey(categoryId);
            cat = catEJB.getCategory();
        return cat;
    } 

    public Product getProduct(String productId, Locale locale) {
        Product prod = null;
        ProductROEntity prodEJB = prodHome.findByPrimaryKey(productId);
        prod = prodEJB.getProduct();
        return prod;
    }

    public Collection getProducts(String categoryId, int start, int count, Locale locale) {

        ArrayList al = new ArrayList();
        CategoryROEntity cat = catgyHome.findByPrimaryKey(categoryId);
        Collection all = prodHome.findByCategory(cat, start, count);
        Iterator it = all.iterator();
        while(it.hasNext()) {
             ProductROEntity prod = (ProductROEntity)it.next();
             al.add(prod.getProduct());
   
    }
   

    public Collection getItems(String productId, int start, int count, Locale locale)    {

        ArrayList al = new ArrayList();

        ProductROEntity prod = prodHome.findByPrimaryKey(productId);
        Collection all = itemHome.findByProduct(prod, start, count);
        Iterator it = all.iterator();
        while(it.hasNext()) {
             ItemROEntity item = (ItemROEntity)it.next();
             al.add(item.getItem());
        }
        return al;
    } 

    public Item getItem(String itemId, Locale locale) {
        Item item = null;
        ItemROEntity itemEJB = itemHome.findByPrimaryKey(itemId);
        item = itemEJB.getItem();
        return item;
    } 

    public Collection searchProducts(Collection keyWords, int start, int count, Locale locale) {

        ArrayList al = new ArrayList();
        Collection all = prodHome.findByKeywords(keyWords, start, count);
        Iterator it = all.iterator();
        while(it.hasNext()) {
                ProductROEntity prod = (ProductROEntity)it.next();
                al.add(prod.getProduct());
        }
        return al;
    }
}

Le code précédent est propre dans la mesure où il ne contient aucune requête SQL ou référence technique vers des objets de type JDBC. Les objets manipulés dans chaque méthode sont des EJB Entité, représentation d'objets métier stockés en base. Attardons nous maintenant sur la classe Product du PetShop apparemment destinée à jouer le même rôle.

Premier Anti-Pattern : "Ne jamais mélanger Accès aux données et couche de service"

Analysez attentivement le code suivant et comparez le à son équivalent PetStore. Pour conviendrez qu'il est très difficile de qualifier la nature d'une telle classe. Est-ce un composant métier persistant ou un composant de service ? Les deux apparemment puisqu'on y trouve l'ensemble des propriétés d'un Produit associés à un ensemble de services tels que la recherche d'un produit.

using System;
using
System.Collections;
using
System.Data;
using
System.Data.SqlClient;
using
System.Text;
using
System.Web;
using
System.Web.Caching;
using
System.Configuration; 

namespace PetShop.Components {
      public class Product {
            private const string CACHE_KEY_PRODUCTS = "Products";
            private const string CACHE_KEY_PRODUCTS_BY_CATEGORY = "ProductsByCategory:";
            private const string SQL_SELECT_PRODUCTS = "SELECT ProductId, Name, Descn FROM Product";
            private const string SQL_SELECT_PRODUCTS_BY_CATEGORY = "SELECT ProductId FROM Product WHERE
 Category = @Category";
            private const string SQL_SELECT_PRODUCTS_BY_CATEGORYb = "SELECT ProductId, Name, Descn FROM
Product WHERE Category = @Category";
            private const string SQL_SELECT_PRODUCTS_BY_SEARCH1 = "SELECT ProductId FROM Product WHERE ((";
            private const string SQL_SELECT_PRODUCTS_BY_SEARCH1b = "SELECT ProductId, Name, Descn FROM
Product WHERE ((";
            private const string SQL_SELECT_PRODUCTS_BY_SEARCH2 = "LOWER(Name) LIKE '%' + {0} + '%' OR
LOWER(Category) LIKE '%' + {0} + '%'";
            private const string SQL_SELECT_PRODUCTS_BY_SEARCH3 = ") OR (";
            private const string SQL_SELECT_PRODUCTS_BY_SEARCH4 = "))";
            private const string PARM_CATEGORY = "@Category";
            private const string PARM_KEYWORD = "@Keyword";
            private static readonly ArrayList EMPTY_DATASOURCE = new ArrayList();
            public static readonly string[] CACHE_DEP_PRODUCTS = new string[] { CACHE_KEY_PRODUCTS };
            private string id;
            private string name;
            private string description; 

            public Product(string id, string name, string description) {
                  this.id = id;
                  this.name = name;
                  this.description = description;
            }

            public string Id {
                  get { return id; }
            } 

            public string Name {
                  get { return name; }
            }

            public string Description {
                  get { return description; }
            }

            public static IList GetProductsByCategory(string category) {     

                  IList productsByCategory = new ArrayList();
                  SqlParameter parm = new SqlParameter(PARM_CATEGORY, SqlDbType.Char, 10);
                  parm.Value = category;                

                  using (SqlDataReader rdr = Database.ExecuteReader
            (Database.CONN_STRING1, CommandType.Text, SQL_SELECT_PRODUCTS_BY_CATEGORYb, parm)) {
                   while (rdr.Read()){
                      Product product = new Product(rdr.GetString(0), rdr.GetString(1), rdr.GetString(2));
                      productsByCategory.Add(product);
                   }
                }

              return productsByCategory;
            } 

            public static IList GetProductsBySearch(string text) {
                  if (text.Trim() == string.Empty) return EMPTY_DATASOURCE;
                  IList productsBySearch = new ArrayList();
                  string[] keywords = text.Split();
                  int numKeywords = keywords.Length;
                  StringBuilder sql = new StringBuilder(SQL_SELECT_PRODUCTS_BY_SEARCH1b); 

                  for (int i = 0; i < numKeywords; i++) {
                             sql.Append(string.Format(SQL_SELECT_PRODUCTS_BY_SEARCH2, PARM_KEYWORD + i));
                             sql.Append(i + 1 < numKeywords ? SQL_SELECT_PRODUCTS_BY_SEARCH3 :
                                                    SQL_SELECT_PRODUCTS_BY_SEARCH4);
                   }
                   string sqlProductsBySearch = sql.ToString();
                   SqlParameter[] parms = Database.GetCachedParameters(sqlProductsBySearch);
                   if (parms == null) {
                          parms = new SqlParameter[numKeywords];
                          for (int i = 0; i < numKeywords; i++)
                             parms[i] = new SqlParameter(PARM_KEYWORD + i, SqlDbType.VarChar, 80);
                             Database.CacheParameters(sqlProductsBySearch, parms);
                   }

                   for (int i = 0; i < numKeywords; i++)
                          parms[i].Value = keywords[i];

                   using (SqlDataReader rdr = Database.ExecuteReader(Database.CONN_STRING1,
                                        CommandType.Text, sqlProductsBySearch, parms)) {
                   while (rdr.Read()){
                       Product product = new Product(rdr.GetString(0), rdr.GetString(1),rdr.GetString(2));
                       productsBySearch.Add(product);
             
          }
           
       }

                return productsBySearch;
           
}
      }
}

Product.cs

Cette conception va l'encontre des règles élémentaires d'architecture qui imposent la séparation claire entre couche de service et couche d'objet métier. Ici nous avons un objet hybride qui réalise trop de tâches et dont le code augmentera de manière exponentielle au fur et à mesure des ajouts successifs de nouveaux services. D'ailleurs, la présence de code ADO.NET ajoute à la confusion générale car, là encore, la règle d'indépendance entre objet de service et Framework technique n'est pas respectée. Le jour où il sera nécessaire de modifier d'éventuelles requêtes SQL, l'opération sera un vrai cauchemar tant la partie accès aux données est diluée dans de multiples classes.

Par ailleurs, si le Java PetStore intègre un objet appelé CatalogEJB dont le rôle est d'assurer la gestion du catalogue Produit, il y a une bonne raison à cela. Lors de la conception d'une application, le première opération consiste à identifier les objets de service qui auront pour tâche de gérer un domaine applicatif bien particulier. Ce type d'objet s'appelle aussi une Façade fonctionnelle. En règle générale les méthodes utilisées dans une façade fonctionnelle sont transactionnelles car elles mettent en oeuvre plusieurs sous traitements atomiques (ACID). Exemple : 

 

L'intérêt principal consiste à réutiliser des briques de DAO en fonction du type des méthodes. Imaginez une création de Category. L'utilisateur commence par créer la Category principale puis l'ensemble des Produits associés à cette Category. Si un de ces deux traitements échoue, il faudra annuler l'ensemble des opérations car l'atomicité n'est pas respectée.

Si on revient sur l'implémentation du PetShop, on s'aperçoit qu'aucune DAO n'est utilisée et que toutes les opérations sont directement effectuées en base à l'intérieur des méthodes de la classe Product, ce qui exclue de facto la possibilité de réutiliser ces traitements dans d'autres objets. C'est le cas notamment de la méthode GetProductsBySearch().

L'intérêt d'une couche DAO est de permettre la modularité et la réutilisabilité de la couche de données tout en assurant son indépendance technique par rapport à la couche de service. Le PetShop ne respecte pas ces règles élémentaires, nous y reviendrons largement dans la suite.

La séparation des couches n'est pas respectée

La seconde chose qui nous frappe lorsqu'on étudie les sources du PetShop est la légèreté avec laquelle est gérée la séparation des couches. Lorsqu'on implémente une architecture multi-tiers, les opérations doivent toujours s'effectuer de la couche la plus haute vers la couche la plus basse. En d'autres termes, il est strictement interdit de référencer la couche de présentation lorsque vous êtes dans la couche de service. Si d'aventure l'application doit évoluer pour passer du client léger (ASPX) au client lourd (WinForms), aucun changement dans la couche de service ne doit être nécessaire.

Voici ce qu'on retrouve dans la classe Order.cs. En théorie, un objet de ce type ne doit contenir aucun lien vers des API techniques.

using System;
using
System.Collections;
using
System.Web; (!!)
using
System.Xml.Serialization; 

namespace PetShop.Components {
      public class Order {
            private const string KEY_ORDER = "Order";
            private const string URL_CART = "ShoppingCart.aspx"; 

            public int orderId;
            public DateTime date;
            public string userId;
            public string cardType;
            public string cardNumber;
            public string cardExpiration;
            public Address billingAddress;
            public Address shippingAddress;           

            [XmlArrayItem("item", typeof(LineItem))]
            public ArrayList lineItems; 

            [XmlIgnore]
            public Cart cart = new Cart(); 

            [XmlIgnore]
            public static Order MyOrder {
                  get {
                        Order myOrder = HttpContext.Current.Session[KEY_ORDER] as Order;
                        if (myOrder == null)
                             myOrder = MyOrder = new Order();
                        return myOrder;
                  }
                  set { HttpContext.Current.Session[KEY_ORDER] = value; }
            } 

            public void Validate() {
                  if (cart.Count < 1)
                        HttpContext.Current.Response.Redirect(URL_CART);
            }                       

      }

}

Cherchez l'intrus :-). Il va de soi que l'utilisation de l'objet HttpContext et de l'ordre import System.Web est strictement inutile et surtout dangereuse pour une application à ce niveau de l'architecture. Non seulement la propriété Session de type Singleton est utilisée comme variable globale pour stocker la Facture courante, mais ce type de code est source d'erreurs souvent difficiles à déceler dans un programme en raison des effets de bords engendrés par de telles manipulations. Sans compter la variable URL_CART qui n'est pas du tout à sa place.

C'est le rôle de la couche de présentation que de piloter la couche de service et non l'inverse. Quant aux flux alternatifs, il existe des outils appropriés dans ASP.NET permettant de gérer cela de manière efficace, un Response.Redirect n'a rien à faire dans un objet métier.

Pas de véritable objet Métier   

Un objet métier représente une entité du modèle du domaine UML. Lorsque vous représentez votre diagramme de classes, les objets métier sont soit :

  1. Des objets fournissant un service : en règle générale, ce sont des contrôleurs de UseCase.

  2. Des objets persistants : Une Facture, un Bon de Commande, un Compte utilisateur, etc ....

Dans le Java PetStore, ces objets sont identifiés à l'aide des EJB Session et des EJB Entité. Voyons comment le PetShop implémente les objets métier persistants (Entité). L'exemple le plus représentatif est la classe Item.cs. Un Item dans l'application est un produit possédant plusieurs propriétés (Name, ProductName, Description, Price, ...). Dans le PetShop, un Item est persistant dans une table appelée Item.

Voici le code source de la classe Item.cs. Non seulement, le code technique d'accès aux données est mélangé aux propriétés de l'objet, mais Item est considéré apparemment comme un objet de service puisque la couche de présentation y fait appel :

      override protected void OnLoad(EventArgs e) {
                  item = Item.GetItem(Request[KEY_ITEM_ID]);                 

                  if (item.Quantity > 0)
                        lblQty.Text = item.Quantity.ToString();
                  else {
                        lblQty.Text = MSG_BACK_ORDERED;
                        lblQty.CssClass = CSS_ALERT;
                  }
            }

ItemDetails.aspx

Ce type d'utilisation est à proscrire. Pour résumer, les pages ASPX font appel directement à des objets persistants. C'est comme si en Java les pages JSP faisaient directement appel aux EJB Entity. Cela revient à court-circuiter entièrement la couche de service. Vous comprenez maintenant pourquoi l'objet CatalogEJB dans le Java PetStore prend en charge les traitements liés aux produits mais aussi aux Items. Les pages JSP passent toujours par une façade fonctionnelle représentée par l'objet Catalog assurant la logique transactionnelle. Le PetShop aurait dû suivre la même règle, au lieu de cela, il ignore tout simplement ce Design Pattern.

Pour finir, il est à noter que la méthode GetItem(ItemID) de la classe Item est statique (static). Les auteurs du PetShop semblent ignorer là encore le Design Pattern Factory dont le rôle est justement de fournir des méthodes statiques permettant de rechercher, insérer ou supprimer d'autres objets. Cette conception est un mélange hybride entre un objet de service, objet persistant et DAO. Il va s'en dire que cette approche réduit drastiquement le nombre de lignes de code mais s'avère totalement inapproprié dans une telle application.

Pas de couche d'accès aux données 

Nous avons déjà abordé cette carence lors de précédentes remarques mais attardons nous un instant sur ce concept pourtant largement documenté sur le site MSDN.

Nous n'entrerons pas dans le pourquoi d'une couche d'accès aux données. Cet article décrit très largement l'intérêt d'une DAL (Data Access Layer) dans une architecture multi-tiers. Le fait est que le PetShop n'implémente pour ainsi dire aucune couche de données. Les exemples précédents illustrent bien la dilution des accès SQL dans l'application. Seule une classe Database.cs joue le rôle de Helper pour l'accès aux données mais ne représente en aucun cas une classe de type DAO.

Pourtant, à bien y regarder de près, le PetShop contient par endroit un semblant de code faisant penser que les auteurs de l'application ont voulu simuler une création de modèle objet. Dans la classe précédente Product.cs, la méthode GetProductBySearch(..) renvoi bien au client un objet de type Product correspondant à la classe courante. Malheureusement, l'utilisation qui en est faite est assez maladroite. En effet, le client, en l'occurrence les pages ASPX, ont une totale visibilité sur l'implémentation d'un objet de service. La règle fondamentale d'encapsulation n'est pas respectée, la notion d'interface n'est pas jamais mise à profit dans cette application alors que le Java PetStore en fait une utilisation intensive à travers les EJB. Chaque composant du Java PetStore possède une interface, une Factory et une implémentation. Le PetShop ne possède aucune Interface. Avouez qu'il y a de quoi s'interroger.

La solution DAO

Il existe plusieurs solutions à ce type de problème :

- Implémenter une couche de persistance à l'aide d'objets DAO

- Faire appel à un outil de mapping Objet/Relationnel tel que ObjectSpaces ou DTM

La première solution est la seule envisageable à l'heure où nous écrivons ces lignes car ObjectSpaces n'est toujours pas disponible. Voici comment implémenter le Design Pattern DAO en C#. Trois classes sont nécessaires, la Factory, l'implémentation de la DAO (SQL Server) et l'interface.

using System;

namespace PetShop.DataAccessObjects {

      /// <summary>
      /// DAOFactory is responsible in creating DAO impl
      /// </summary>
      public class DAOFactory {
            public static IProductDAO GetDAO(){
                  return new ProductDAO();
            }
      }

}

ProductFactoryDAO.cs

 Voici maintenant la classe DAO proprement dite réalisant les accès en base.

using System;
using
System.Data ;
using
System.Data.SqlClient;
using
PetShop.BusinessModel ;
using
System.Collections ; 

namespace PetShop.DataAccessObjects {

      /// <summary>
      /// Description résumée de ProductDAO.
      /// </summary>

      public class ProductDAO : IProductDAO {

            public static ProductDAO instance = null ;
            private const string PARM_CATEGORY = "@Category";
            private const string SQL_SELECT_PRODUCTS_BY_CATEGORYb = "SELECT ProductId, Name, Descn FROM
Product WHERE Category = @Category"; 

            protected ProductDAO() {}

            public IList FindProductsByCategory(string category) {
                  IList productsByCategory = new ArrayList();
                  SqlParameter parm = new SqlParameter(PARM_CATEGORY, SqlDbType.Char, 10);
                  parm.Value = category;

                  SqlDataReader rdr = Database.ExecuteReader(Database.CONN_STRING1, CommandType.Text,
SQL_SELECT_PRODUCTS_BY_CATEGORYb, parm) ;

                  while (rdr.Read()){
                        Product product = new PetShop.BusinessModel.Product(rdr.GetString(0),
rdr.GetString(1), rdr.GetString(2));
                        productsByCategory.Add(product);
                  }
                  return productsByCategory;
            }
      }

}
ProductDAO.cs

Nous vous passons l'interface de la DAO qui contient essentiellement les méthodes de type Finder (FindXX, Update, ...).

Il ne nous reste plus qu'à faire appel à cet objet dans la couche de service. Pour ce faire, il est nécessaire de créer un objet CatalogService équivalent à CatalogEJB du Java PetStore.

using System;
using
PetShop.BusinessModel ;
using
PetShop.DataAccessObjects ;
using
System.Collections ;

namespace PetShop.Components {

      /// <summary>
      /// CatalogService component
      /// </summary>

      public class CatalogService {

            // Retrieve all Products by using specific DAO
            public IList GetProductsByCategory(String categoryId){
                  IProductDAO factory = ProductDAOFactory.GetDAO();
                  return factory.FindProductsByCategory(categoryId);
            }
            // … Others Catalog Service methods : GetItems(..),

            // GetCategories(), GetProductsBySearch etc …

     }          
}
CatalogService.cs

La lecture de ce code se rapproche sensiblement de l'équivalent Java. Nous avons donc mis ici en place une véritable couche d'accès aux données sans aucune mesure avec le code existant dans le PetShop. Les DAO peuvent être réutilisées dans d'autres portions de l'architecture ou simplement enrichies en minimisant les impacts sur la couche de service. Bien entendu, il reste encore à propager le contexte transactionnel, mais cela est du ressort du Framework technique.

Des erreurs dans la conception du modèle objet   

Le plus étonnant lorsqu'on compare le PetStore et le PetShop est la différence de conception adoptée de part et d'autre. Si fonctionnellement les deux applications sont égales, le PetShop ne peut être assimilé à une "Best Practice" en terme de conception. Regardons de plus près le modèle objet adopté par le Java PetStore. Voici le fameux module Catalog.

   Copyright Sun Microsystems

Le Module Catalog contient plus d'une dizaine de classes. Il est à noter que le client ne possède jamais de référence directe vers des objets persistants tels qu'un Product ou une Category. Seul l'objet Catalog est habilité à manipuler ce type d'objet.

Nous avons cherché dans la documentation officielle du PetShop sur MSDN un lien vers une éventuelle description du modèle objet utilisé par Microsoft. Les seuls schémas présentés dans l'article sont ceux des Modèles physiques de données illustrant les différentes tables de l'application. Les auteurs du PetShop ont semble-t-il adopté une démarche orientée données sans analyse et conception préalable en objet. Cela pourrait expliquer certaines carences en terme d'architecture.

Nous avons tout de même essayé par rétro-conception d'extraire le modèle objet complet. Le résultat est assez effrayant ;-). Contrairement au PetStore, il n'y aucun point d'entrée et tous les objets peuvent être potentiellement référencés par la couche de présentation (ASPX). Sans compter que la couche de Service pointe sur le namespace PetShop.Web et inversement.  

 

 

 

Excepté l'impression générale d'incohérence, quelques détails ont attiré notre attention. Pourquoi cette relation d'association entre les classes Order et Cart ? Le premier objet représente une entité persistante, le second est un objet transitoire. C'est d'ailleurs pour cela que le Java PetStore utilise un EJB Entity pour le premier et un objet StateFul pour le second. D'autre part, fonctionnellement, une Facture n'a pas à être lié à un caddie qui est un objet dynamique. Bref, certains choix sont pour le moins douteux, mais surtout, on comprend mal pourquoi la conception objet du Java PetStore n'a pas été reprise telle quelle dans le PetShop.

Toute l'application dans un seul Namespace

Comment imaginer créer une application de référence en terme de "Best Practice" et implémenter celle-ci dans un seul et même Namespace ? Excepté la couche de présentation située dans PetShop.Web. Toute l'application est gérée dans un seul et même Namespace : PetShop.Components. Tout y est ! de DataBase.cs (qui n'a rien à faire dans un tel Namespace) à Order.cs en passant par Item.cs.

L'élément fondamentale de la séparation des couches est le Namespace. Si aujourd'hui les langages modernes nous permettent de réaliser une encapsulation protégée, c'est en partie grâce aux Namespaces. Le Java PetStore contient des dizaines de packages, le PetShop en contient un. Sans commentaire.

La généricité de la base de données n'est pas assurée  

Il est compréhensible qu'au regard de la nature même d'ADO.NET et sa forte intégration avec SQL Server, Microsoft n'ai pas jugé utile d'implémenter de multiples connecteurs vers d'autres Bases de données. Cependant, DotNetGuru a démontré a de maintes reprises que moyennant une conception stricte et générique il était possible de concevoir une application indépendamment du SGBD sous-jacent. A ce titre, le Java PetStore (décidément) est basé sur JDBC et fonctionne parfaitement quelque soit la base. Nous regretterons donc ce choix car une conception basée sur des DAO aurait nécessité un effort minimal.

Aucune utilisation d'objets DataSet    

Dans les sources du PetShop, quelques points bien précis ont attiré notre attention. Tout d'abord, la classe DataBase.cs qui aurait dû se trouver dans un Namespace PetShop.DAL.Util fourni un ensemble de méthodes destinées à exécuter des requêtes SQL et à renvoyer le résultat à travers des SQLDataReader (curseurs en mode connecté). Or lorsqu'on analyse les classes faisant appel à la méthode ExecuteReader() de l'objet DataBase, elles s'attachent à remplir des collections d'objet afin de les renvoyer par valeur. Quel est l'intérêt dans ce cas d'utiliser un ExecuteReader() ? Les DataSet typés ont été créés à cet effet et représentent une avancée majeure dans la programmation en mode déconnecté. Pourquoi s'en priver et réinventer la poudre alors qu'ils peuvent être judicieusement mis à profit pour implémenter le Design Pattern Value-Object permettant de renvoyer des collection d'objets par valeur ? Autant de questions qui restent sans réponse ...

Aucune gestion d'Exception    

La gestion des exceptions est la première remarque faite à l'encontre du PetShop par la communauté Java. Il est évident que cette particularité démarque clairement l'implémentation du PetStore de celle du PetShop. La gestion d'erreur est fondamentale dans la conception d'architectures multi-tiers distribuées ou non. Même si par défaut .NET fait remonter la plupart des exceptions à l'appelant, ce mode de gestion n'est pas du tout adapté dans certains cas. Prenons l'exemple d'un produit recherché par un utilisateur. Si ce produit n'existe pas, le PetShop se soldera par un NullReferenceException qui plongera l'utilisateur dans un trouble profond ;-). Le Java PetStore, quant à lui, gèrera une exception de type ObjectNotFound et renverra proprement au client une exception métier de type ProductNotFound. Il y a un travail de transformation à réaliser lorsqu'on propage une exception entre les différentes couches d'une architecture et aucun Framework technique n'est aujourd'hui en mesure de réaliser automatiquement cette tâche.

Les transactions distribuées

La gestion des transactions est pour le moins des plus surprenantes. Là encore nous nous attendions à la propagation d'un contexte entre plusieurs invocations de méthodes transactionnelles. Il n'en est rien, le contexte transactionnel reste confiné à la classe OrderCOM.cs et se résume à deux ordres d'insertions SQL dans deux bases de données distinctes. Même si l'on peut raisonnablement comprendre que le PetShop doit rester une application simple, ce fonctionnement est trop sommaire pour prétendre servir de modèle de référence.

L'implémentation du WebService OrderWebService.cs

Le WebService utilisé dans le PetShop est un modèle d'anti-pattern d'architecture. Son but est d'extraire de la base une facture dont l'identifiant est passé en paramètre à travers la méthode GetOrder(string OrderId).

Tout d'abord, dans la mesure du possible, un WebService doit toujours s'appuyer sur sa couche de service qui fourni la plupart du temps le service requis. L'intérêt encore une fois est de réutiliser au maximum les traitements lorsqu'ils sont déjà présents. De plus, dans le cas d'une méthode transactionnelle, cette conception permet de tirer parti des API techniques existantes telles que  ServicedComponent pour .NET et J2EE pour Java. L'exemple suivant illustre le WebService censé fournir la même opération. Jugez-en par vous même, la méthode getOrderDetails() s'appuie sur l'EJB orderAccessEJB, façade fonctionnelle destinée à gérer les Factures. Nous vous laissons comparer ce source avec l'équivalent .NET : OrderWebService.cs. Le résultat est édifiant.

import weblogic.jws.control.JwsContext;
import com.sun.j2ee.blueprints.customer.order.model.WebSrvcOrderModel;
import javax.ejb.*;
import java.rmi.*; 

public class OrderWebSvc {

    private orderAccessControl orderAccessEJB;
    JwsContext context;

    /**
     * @jws:operation
     * @jws:protocol soap-style="rpc"
     */

    public WebSrvcOrderModel getOrderDetails(int orderId){
        WebSrvcOrderModel order=null;
        try {
            order = orderAccessEJB.getOrder(orderId);
        } catch(Exception e) {}
        return order;
    }
}
OrderWebSvc.jws (utilise Apache AXIS)

Conclusion

Il faut bien l'avouer, nous avons été totalement déçu par l'implémentation du PetShop. Pour tout vous dire, nous étions persuadé d'être en possession d'un "Brouillon" du PetShop suite à une erreur de manipulation ;-). Plusieurs téléchargements ont fini par nous convaincre du contraire. L'application de référence proposée par Microsoft comme challenger du Java PetStore se rapproche plus de l'Anti-Pattern d'Architecture que d'une véritable implémentation de référence. Comment en aurait-il pu être autrement. Il faut bien avouer qu'il est difficile de reproduire l'ensemble des concepts architecturaux du Java PetStore avec 10 classes et 7 fois moins de code. L'API Serviced Component est éventuellement moins verbeuse que l'API Enterprise JavaBeans, la syntaxe de C# est assurément plus concise que son homologue Java, mais il convient de raison garder.

Tout cela est d'autant plus étonnant que Microsoft possède sans conteste une véritable expertise dans la conception d'architectures objets. Il suffit de visiter le site Architecture center dans lequel sont abordés les notions de DAL, BLL et Design Pattern pour s'en convaincre. Difficile de comprendre dans ces conditions les raisons d'un tel gâchis. Aucune application, si minime soit-elle ne justifie le sacrifice de son architecture au profit de ses performances.


Auteur : Sami JABER

Copyright : DotNetGuru © Novembre 2002

Note : DotNetGuru travaille actuellement à la réécriture du PetShop dans le but d'implémenter une véritable architecture n-tiers basée sur des DAO. La licence distribuée par Microsoft nous y autorise moyennant certaines conditions très acceptables. Cependant, vu les faibles moyens qui sont les notre, nous ne pouvons nous engager sur de quelconques délais de livraison. Si vous souhaitez vous aussi nous fournir votre aide dans cette entreprise, n'hésitez pas à nous contacter, la tâche étant rude.

 

La licence du PetShop

1. GRANT OF LICENSE.

 General. Microsoft grants you the right to make, use and modify the sample source code contained in the Product (the “Sample Code”) for the purposes of evaluating, designing, developing, and testing software product(s), and to reproduce and distribute the Sample Code, along with any modifications thereof, provided that you agree: 

(a) to distribute the Sample Code in source or object code form and only in conjunction with and as a part of a software application product developed by you that adds significant and primary functionality to the Sample Code; 

(b) to not use Microsoft’s name, logo, or trademarks to market your software application product; 

(c) to include a valid copyright notice on your software product; and 

(d) to indemnify, hold harmless, and defend Microsoft from and against any claims or lawsuits, including attorney’s fees, that arise or result from the use or distribution of your software application product.

(...)