L'architecture multicouche mise en œuvre surune application Web ASP.Net

Ou comment, grâce à un modèle multicouche d'une solution .Net, rendre son code modulaire et facile à maintenir.

Immobilis

Cet article aborde la réalisation d'une application ASP.Net selon une architecture multicouche. Pas à pas, nous verrons comment faire évoluer une simple page Web vers cette architecture robuste. Commentez

- Titre : L'architecture multicouche mise en œuvre surune application Web ASP.Net

- Auteur : Immobilis

- Parution : 20 octobre 2010

- Mise à jour : 20 octobre 2010

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2010 Julien. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- I. Présentation

- II. Le concept du modèle multicouche

- III. Mise en pratique

- III-A. Création de l'application Web

- III-B. Création de la couche de la logique métier

- III-C. Création du projet des objets de transfert de données

- III-D. Création de la couche d'accès aux données

- IV. Pour aller plus loin

- IV-A. Améliorer encore les performances de l'interface homme machine

- IV-A-1. Connecter la source de données sur un « GridView »

- IV-A-2. Connecter la source de données sur un « Repeater »

- IV-B. Plusieurs interfaces homme-machine ou machine-machine grâce à la modularité

- IV-B-1. Une application Console

- IV-B-2. Un service Web avec Windows Communication Foundation (WCF)

- IV-C. Ajouter une gestion de mots de passe

- IV-D. Maintenance en mémoire des données importantes

- IV-E. Changement de la DAL

- V. Conclusion

- VI. Références

- Remerciements

I - Présentation

C'est un lieu commun de dire que les questions d'architecture sont importantes. Il est effectivement primordial de savoir quelle architecture utiliser avant de se lancer dans des développements effrénés. Cela permet d'éviter ceci :

Une fois que le choix est fait, les développements sont beaucoup plus méthodiques, les classes sont plus indépendantes et la maintenance est facilitée.

Dans cet article, je n'aborderai que le modèle multicouche appliqué à l'opération de base de lecture du CRUD (Create, Read, Update, Delete). Il y a déjà beaucoup à dire.

Faisons un petit point de terminologie. Comme je me suis laissé abuser par le terme "tier", je me permets de préciser que "tier" en anglais ne signifie pas la même chose que "tiers" en français. En anglais, "tier" signifie "palier". On peut aussi bien dire "3-tiers" que "3-layers". En français c'est un peu plus confus. On admettra donc que "tiers" en français ne signifie pas "couche" mais composant matériel. Ainsi, en français, une application sur une architecture "3-tiers" est répartie sur trois matériels distincts.

II - Le concept du modèle multicouche

Il y a beaucoup de littérature à ce sujet sur Internet et de discussions passionnantes sur Développez. Pour faire simple, il faut juste retenir les points suivants :

- les couches les plus connues sont : l'interface homme-machine (IHM), les processus métiers (Business Logic Layer, BLL), l'accès aux données (Data Access Layer, DAL) ;

- chaque couche regroupe les composants (objets, méthodes) partageant les mêmes fonctionnalités, les mêmes rôles. Ainsi, en respectant cette architecture, on ne devrait jamais voir une seule requête SQL dans une page Web ;

- l'implémentation d'une couche métier indépendante de l'interface permet de créer facilement une couche de services destinés à l'IHM ou d'autres logiciels ;

- chaque couche ne peut communiquer qu'avec celle qui lui est immédiatement voisine ;

- les couches communiquent entre elles en se transmettant des objets de transfert de données : les DTO ;

- la modularité et le faible couplage de l'approche multicouche sont une formidable contrepartie à l'apparente complexité de son implémentation.

On peut donc représenter cette architecture de base avec ce schéma :

Cette répartition des tâches permet de découpler les classes. Ainsi chaque classe a un périmètre de responsabilités précis reposant le moins possible sur d'autres classes. Chaque méthode exécute une série d'opérations aussi simples que possible. Les données circulent donc sur une chaîne et changent d'état au fur et à mesure.

Dans les paragraphes suivants, nous allons créer une petite application affichant une liste de noms contenus en base de données. Nous allons faire évoluer ce programme vers un modèle multicouche. Au cours de ce processus, nous nous servirons de l'analyseur de performance du code de Visual Studio 2010 pour mesurer précisément la qualité de notre travail.

Nous n'aurons pas réellement besoin de la base car nous ne ferons pas tourner l'application mais analyserons juste le code. Toutefois, si vous voulez un support, vous pouvez reprendre la table utilisée dans ce tutoriel.

III - Mise en pratique

III-A - Création de l'application Web

Je passe rapidement sur les détails de la création de la solution. Créez une nouvelle solution avec une application Web vide. Ajoutez une page Web. Nommez-la « Default.aspx ». Vous obtenez ceci :

Nous allons d'ores et déjà mesurer la performance de notre projet pour l'étalonner.

Visual Studio nous livre les résultats suivants :

Indice de maintenance : 100. Excellent résultat ! Continuons.

Nous allons maintenant ajouter de quoi récupérer nos données en base. Grâce à des objets SQL, nous allons récupérer toutes les colonnes de notre table [Persons] et boucler sur tous les enregistrements et tous les champs pour les écrire dans la page. Voici le code :

Code de la méthode ''Page_Load''

protected void Page_Load(object sender, EventArgs e)
{
using (SqlConnection cn = new SqlConnection())
{
cn.ConnectionString = ConfigurationManager.ConnectionStrings["ChaineDeConnexion"].ConnectionString;
cn.Open();

using (SqlCommand cmd = new SqlCommand())
{
cmd.CommandText = "Select * from [Persons]";
cmd.Connection = cn;

using (SqlDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
for (int i = 0; i < rdr.FieldCount; i++)
{
if (rdr[i] != DBNull.Value)
Response.Write(rdr[i].ToString());
else
Response.Write("NULL");
if (i < rdr.FieldCount)
Response.Write("|");
}
Response.Write("<br />");
}
}
}
}
}

Examinons maintenant nos performances.

Ouch ! La moyenne a baissé à 61 ! Comment allons-nous améliorer ça ? Ce n'est pas critique, mais j'aimerais autant remonter jusqu'à 80.

Commençons par refactoriser notre code. Tout ce que nous attendons de la page au moment de son chargement, c'est de boucler sur une liste d'enregistrements et de l'afficher. Nous allons sortir cette portion de code de la méthode « Page_Load » et le mettre dans une méthode privée. Codé simplement, cela donne :

Code de la méthode ''Page_Load''

protected void Page_Load(object sender, EventArgs e)
{
DataTable dt = LoadData();

for (int i = 0; i < dt.Rows.Count; i++)
{
for (int j = 0; j < dt.Columns.Count; j++)
{
Response.Write(dt.Rows[i][j].ToString());
}
Response.Write("<br />");
}
}

private DataTable LoadData()
{
DataTable dt = new DataTable();

using (SqlConnection cn = new SqlConnection())
{
cn.ConnectionString = ConfigurationManager.ConnectionStrings["ChaineDeConnexion"].ConnectionString;
cn.Open();

using (SqlCommand cmd = new SqlCommand())
{
cmd.CommandText = "Select * from [Persons]";
cmd.Connection = cn;

using (SqlDataReader rdr = cmd.ExecuteReader())
{
if (rdr.Read())
{
dt.Load(rdr);
}
}
}
}

return dt;
}

Quel est le résultat ?

Nous sommes à 65. Ce n'est pas encore ça. Remarquez que la méthode « Page_Load » a un meilleur indice que précédemment. C'est maintenant la méthode « LoadData » qui a un mauvais résultat. Bon, débarrassons-nous d'elle puisqu'elle plombe le score de l'application Web.

III-B - Création de la couche de la logique métier

Ajoutons un projet à notre solution. Il s'agit d'une librairie de classes que nous appellerons « BLL ». Ajoutez-y une classe nommée « PersonManager » et référencez l'espace de nom « System.Configuration ». Voici le code :

Code de la classe ''PersonManager''

public static class PersonManager
{
public static DataTable LoadData()
{
DataTable dt = new DataTable();

using (SqlConnection cn = new SqlConnection())
{
cn.ConnectionString = ConfigurationManager.ConnectionStrings["ChaineDeConnexion"].ConnectionString;
cn.Open();

using (SqlCommand cmd = new SqlCommand())
{
cmd.CommandText = "Select * from [Persons]";
cmd.Connection = cn;

using (SqlDataReader rdr = cmd.ExecuteReader())
{
if (rdr.Read())
{
dt.Load(rdr);
}
}
}
}

return dt;
}
}

Référencez le projet « Bll » dans l'application Web. La méthode « Page_Load » de la page Web devient :

Code de la méthode ''Page_Load''

protected void Page_Load(object sender, EventArgs e)
{
DataTable dt = PersonManager.LoadData();

for (int i = 0; i < dt.Rows.Count; i++)
{
for (int j = 0; j < dt.Columns.Count; j++)
{
Response.Write(dt.Rows[i][j].ToString());
}
Response.Write("<br />");
}
}

Voici l'état de la solution :

Regardons de nouveau notre score une fois que les petits problèmes de compilation ont été résolus.

Pas mal, le score de l'application Web est remonté à 75 ! Une bonne partie de la complexité a été déporté dans la BLL. L'indicateur de complexité cyclomatique est descendu à 4.

La complexité cyclomatique est un indice de mesure de la qualité d'un logiciel qui permet d'évaluer le nombre de chemins de décision d'une méthode. Il est compris entre 1 et 15 (Cyclomatic complexity sur Wikipedia).

Avant d'essayer d'améliorer la BLL, pouvons-nous encore améliorer notre application Web ? Bien entendu. Le code qui boucle sur la « DataTable » a un score de 75. Faisons juste un petit test en changeant d'objet. Troquons notre « DataTable » contre une liste d'objets métier. Pour le besoin du test, nous ferons juste une structure « PersonEntity ». Le résultat est tout aussi significatif que la liste soit remplie par la source de données ou pas. Voici le code :

Test de l'utilisation d'une liste d'objets métier.

public partial class Default : System.Web.UI.Page
{
// Objet métier
public struct PersonEntity
{
public string Id;
public string Nom;
public string Prenom;
}

protected void Page_Load(object sender, EventArgs e)
{
List<PersonEntity> list = new List<PersonEntity>();

foreach (var p in list)
{
Response.Write(string.Format("{0}, {1}, {2}<br />", p.Id, p.Prenom, p.Nom));
}
}
}

Calculons notre score.

Waouh ! Avec une liste, l'application a une note de 90 ! L'utilisation d'une liste est bien moins complexe qu'une « DataTable ». Rien qu'en regardant le code on le devine, une boucle « foreach » est plus simple que deux boucles incrémentées imbriquées.

Il faut donc demander à la BLL de nous envoyer une liste plutôt qu'une « DataTable ». Pour cela, il faut que les deux couches (IHM et BLL) disposent de l'objet métier « PersonEntity ». Pour y parvenir, nous allons créer notre projet DTO.

III-C - Création du projet des objets de transfert de données

Ce projet est très simple et ne contient que des classes appelées « Entité ». Ce ne sont que des coquilles inertes (sans méthode) qui vont permettre aux différentes couches de se transmettre des données.

Ajoutez à la solution un nouveau projet de librairie de classes nommé « DTO ». Ajoutez-y une classe nommée « PersonEntity ». Voici son code :

L'objet de transfert de données ''PersonEntity''

public class PersonEntity
{
public string Id { get; set; }
public string Nom { get; set; }
public string Prenom { get; set; }
}

Ce projet ne doit référencer aucun des autres projets de la solution, car il ne doit pas en dépendre. Sinon, cela génèrera une alerte de référence cyclique. Ce projet est transverse.

La solution se présente maintenant ainsi :

Référencez le projet « DTO » dans l'application Web et la BLL. Résolvez les petits soucis de références. Le code de la méthode « LoadData » de la couche logique métier devient :

La méthode ''LoadData'' dans la couche métier.

public static List<PersonEntity> LoadData()
{
List<PersonEntity> list = new List<PersonEntity>();

using (SqlConnection cn = new SqlConnection())
{
cn.ConnectionString = ConfigurationManager.ConnectionStrings["ChaineDeConnexion"].ConnectionString;
cn.Open();

using (SqlCommand cmd = new SqlCommand())
{
cmd.CommandText = "Select * from [Persons]";
cmd.Connection = cn;

using (SqlDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
PersonEntity p = new PersonEntity();
p.Id = rdr["PersonId"] == DBNull.Value ? string.Empty : rdr["PersonId"].ToString();
p.Nom = rdr["LastName"] == DBNull.Value ? string.Empty : rdr["LastName"].ToString();
p.Prenom = rdr["FirstName"] == DBNull.Value ? string.Empty : rdr["FirstName"].ToString();

list.Add(p);
}
}
}
}

return list;
}

Celui de la page Web :

Code de la méthode ''Page_Load''

protected void Page_Load(object sender, EventArgs e)
{
List<PersonEntity> list = PersonManager.LoadData();

foreach (var p in list)
{
Response.Write(string.Format("{0}, {1}, {2}<br />", p.Id, p.Prenom, p.Nom));
}
}

Mesurons notre score.

L'indice de complexité de la maintenance de l'application Web atteint 80. C'est super, elle est bonne. On la garde. Essayons maintenant d'améliorer la BLL.

III-D - Création de la couche d'accès aux données

Afin de séparer l'accès aux données de la logique métier, nous allons ajouter un nouveau projet : la « DAL ». La DAL n'aura qu'un seul rôle, celui de gérer la relation entre la source de données et la couche métier. Elle reçoit les entités et selon la méthode du CRUD appelée, les données sont insérées, chargées, mises à jour ou effacées. Il arrive que dans certaines architectures les deux se confondent (comme c'est le cas dans notre solution). À mon avis, il est toujours souhaitable, comme nous venons de le voir, de répartir les responsabilités dans des classes différentes. Dans le cas (relativement improbable mais sait-on jamais) où vous souhaitez passer d'une base de données MySQL à Microsoft SQL Server, les efforts à fournir seront beaucoup moins importants si vous avez séparé la BLL de la DAL.

Ajoutez à la solution un nouveau projet librairie de classes nommé « DAL ». Ajoutez une nouvelle classe nommée « PersonProvider ». Référencez le projet DTO dans la DAL et la DAL dans la BLL. N'oubliez pas non plus de référencer « System.Configuration » dans la DAL. Voici le code :

Code du fournisseur de données ''PersonProvider''

public class PersonProvider
{
public List<PersonEntity> LoadData()
{
List<PersonEntity> list = new List<PersonEntity>();

using (SqlConnection cn = new SqlConnection())
{
cn.ConnectionString = ConfigurationManager.ConnectionStrings["ChaineDeConnexion"].ConnectionString;
cn.Open();

using (SqlCommand cmd = new SqlCommand())
{
cmd.CommandText = "Select * from [Persons]";
cmd.Connection = cn;

using (SqlDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
PersonEntity p = new PersonEntity();
p.Id = rdr["PersonId"] == DBNull.Value ? string.Empty : rdr["PersonId"].ToString();
p.Nom = rdr["LastName"] == DBNull.Value ? string.Empty : rdr["LastName"].ToString();
p.Prenom = rdr["FirstName"] == DBNull.Value ? string.Empty : rdr["FirstName"].ToString();

list.Add(p);
}
}
}
}

return list;
}
}

Le code de la BLL devient :

Code de la classe ''PersonManager''

public static class PersonManager
{
public static List<PersonEntity> LoadData()
{
return new PersonProvider().LoadData();
}
}

L'état de la solution est :

Dans cette situation, la BLL paraît presque inutile. Il est vrai que pour le moment, elle joue surtout les intermédiaires. L'avantage est que, par la suite, il sera très simple d'y ajouter du code sans le mélanger avec l'accès aux données.

Voyons le score final en mode Debug :

En mode Release :

IV - Pour aller plus loin

Jusqu'ici, les modifications réalisées donnent plutôt l'impression d'avoir déplacé la complexité de l'application Web jusqu'à la DAL. Cela paraît un peu bête, mais malgré les apparences, c'est un progrès. Vous ne voyez pas pourquoi ? Débarrassés de cette complexité et grâce à la modularité du code, nous allons pouvoir faire évoluer les projets plus facilement. Pour appuyer mon propos, je vous donne des exemples.

IV-A - Améliorer encore les performances de l'interface homme machine

IV-A-1 - Connecter la source de données sur un « GridView »

On peut encore améliorer notre score si on remplace la boucle « foreach » par un « GridView » sur lequel on « Bind » la méthode « BLL.LoadData » :

Code behind de la page Web et un GridView

public partial class Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
GridView1.DataSource = PersonManager.LoadData();
GridView1.DataBind();
}
}

IV-A-2 - Connecter la source de données sur un « Repeater »

Cette autre écriture permet de faire grimper l'indice de maintenabilité jusqu'à 94 avec un « Repeater » et le code suivant :

Code balisé de la page Web

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication.Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Repeater ID="Repeater1" runat="server" DataSource='<%# this.List %>'>
<ItemTemplate>
<%# (Container.DataItem as DTO.PersonEntity).Id %>,
<%# (Container.DataItem as DTO.PersonEntity).Prenom %>,
<%# (Container.DataItem as DTO.PersonEntity).Nom %><br />
</ItemTemplate>
</asp:Repeater>
<asp:GridView ID="GridView1" runat="server">
</asp:GridView>
</div>
</form>
</body>
</html>

Code behind de la page Web, un ''Repeater'' et une propriété

public partial class Default : System.Web.UI.Page
{
protected List<PersonEntity> List = PersonManager.LoadData();

protected void Page_Load(object sender, EventArgs e)
{
this.DataBind();
}
}

Voici les indices de performance.

L'application Web a un score de 94 !

IV-B - Plusieurs interfaces homme-machine ou machine-machine grâce à la modularité

Grâce à l'absence de spécialisation envers un type d'applications, la méthode « LoadData() » peut aussi bien être appelée d'une application console que d'une page Web ou que d'un Service Web.

IV-B-1 - Une application Console

Nous avons vu précédemment la facilité avec laquelle nous pouvions afficher nos données dans une application Web. Eh bien, c'est à peine plus compliqué d'utiliser la couche métier dans une application console.

Ajoutez un projet console dans la solution. Référencez-y les projets BLL et DTO. Voici le code nécessaire à l'affichage du contenu de la table [Persons] dans la console :

Affichage du contenu de la table [Persons] dans une application console.

using System;
using System.Collections.Generic;
using Bll;
using DTO;

namespace ConsoleApplication
{
class Program
{
static void Main(string[] args)
{
List<PersonEntity> list = PersonManager.LoadData();

foreach (var p in list)
{
Console.WriteLine(string.Format("{0}, {1}, {2}", p.Id, p.Prenom, p.Nom));
}
Console.ReadLine();
}
}
}

Le seul changement dans le code est le remplacement de « Response.Write » par « Console.WriteLine ». Comment aurions-nous fait avec le code SQL directement dans la page Web  ?

IV-B-2 - Un service Web avec Windows Communication Foundation (WCF)

Allez, soyons fous, puisque nous utilisons Visual Studio 2010 et le framework 4.0, ajoutez un projet « WCF Service Library » nommé « WcfServiceLibrary » à la solution :

Référencez-y le projet BLL et DTO. Par défaut, Visual Studio ajoute deux fichiers : une interface « IService1.cs » et une classe « Service.cs ». Renommez-les en « IThreeTierServiceLibrary.cs » et « ThreeTierServiceLibrary.cs ». Vérifiez que le fichier de configuration contient bien la chaîne de connexion.

Modifiez la classe « PersonEntity » du projet DTO afin d'ajouter les attributs nécessaires à l'exécution du service WCF. Pour que les attributs soient reconnus, n'oubliez pas d'ajouter une référence à la dll « System.Runtime.Serialization ».

Ajout des attributs nécessaires au DTO ''PersonEntity'' pour l'utilisation de WCF

[DataContract]
public class PersonEntity
{
[DataMember]
public string Id { get; set; }
[DataMember]
public string Nom { get; set; }
[DataMember]
public string Prenom { get; set; }
}

Modifiez le code de l'interface « IThreeTierServiceLibrary.cs » pour ne laisser que le code suivant :

Code de l'interface ''IThreeTierServiceLibrary''

namespace WcfServiceLibrary
{
[ServiceContract]
public interface IThreeTierServiceLibrary
{
[OperationContract]
PersonEntity GetPersonByName(string name);
}
}

Modifiez le code de la classe « ThreeTierServiceLibrary.cs » pour ne laisser que le code suivant :

Code de la classe ''ThreeTierServiceLibrary''

namespace WcfServiceLibrary
{
public class ThreeTierServiceLibrary : IThreeTierServiceLibrary
{
public PersonEntity GetPersonByName(string name)
{
return PersonManager.LoadData().Where(x => x.Nom.ToLowerInvariant() == name.ToLowerInvariant()).FirstOrDefault();
}
}
}

Ce code fait appel à notre BLL et à une expression « lambda » pour sélectionner la première entité de la liste dont la propriété « Nom » aura la valeur du paramètre « name ».

Déclarez le projet WCF comme projet de démarrage. Pressez la touche F5 pour lancer le programme « WcfTestClient.exe » sur la bibliothèque. La solution compile et l'écran suivant apparaît :

Ce logiciel permet de tester les méthodes d'un service Windows Communication Foundation. Notez que nous sommes sur le port 8732. Sélectionnez la méthode « GetPersonByName » dans l'arborescence. La partie droite de la fenêtre fait apparaître un formulaire dans lequel vous pouvez saisir des paramètres pour faire fonctionner le service WCF. Saisissez « terieur », le nom de famille d'Alex, dans le cadre « Request ». Pressez le bouton « Invoke ». Le résultat s'affiche dans le cadre « Response ».

Nous pouvons maintenant intégrer cette classe dans un site ou une application Web.

Référencez le projet « WcfServiceLibrary » dans l'application Web. Ajoutez-y un élément « service WCF » nommé « ThreeTierService.svc ». Supprimez le code behind ainsi que le fichier de l'interface. Nous n'en aurons pas besoin. Affichez le code balisé du service (clic droit >> View Markup). Modifiez-le en supprimant la totalité de l'attribut « CodeBehind » et en changeant la valeur de l'attribut « Service » de « WebApplication.ThreeTierService » à « WcfServiceLibrary.ThreeTierServiceLibrary ».

Code balisé du service WCF ''ThreeTierService''

<%@ ServiceHost Language="C#" Debug="true" Service="WcfServiceLibrary.ThreeTierServiceLibrary" %>

Testons l'intégration de notre bibliothèque WCF en navigant sur la page « ThreeTierService.svc ». Faites un clic droit >> Afficher dans le navigateur. La page du service s'ouvre et si tout va bien, vous devriez voir ceci:

Remarquez que nous sommes maintenant sur le port 50418. La librairie WCF est bien intégrée sur l'application Web.

Je crois que la preuve de l'efficacité de cette architecture est faite. Grâce à la modularité de la couche métier, nous avons pu facilement créer une application Web, Console, une librairie de classe WCF et service Web WCF sans répéter de code. De plus, la solution conserve un excellent indice de maintenance.

Normalement, il ne devrait pas y avoir trop de soucis sur la configuration des applications. En effet, pour ma part, Visual Studio 2010 a bien géré la mise à jour des configurations au fur et à mesure. Toutefois, si vous rencontrez des problèmes de compilation ou d'exécution, je vous invite à visionner ce tutoriel (dans tous les cas, je vous le conseille) : Hosting WCF Services in IIS, de Cliff Simpkins.

IV-C - Ajouter une gestion de mots de passe

J'ai dans mon projet « VideoCrossing » une classe « UserManager » qui gère les opérations du CRUD sur ma table des utilisateurs. J'ai besoin, en plus, de méthodes pour recréer des mots de passe, valider un compte. Ces méthodes ne peuvent pas être placées ailleurs que dans la couche métier. Admettons qu'un internaute qui utilise mon site souhaite générer un nouveau mot de passe parce qu'il a oublié le sien. Voici le cas d'utilisation :

- l'internaute saisit son adresse email dans un champ texte ;

- il clique sur le bouton « Générer un nouveau mot de passe » ;

- la page Web appelle la méthode « BLL.UserManager.GenerateNewPassword(mailAddress) » ;

- celle-ci charge le compte de l'utilisateur grâce à « BLL.UserManager.Load(new MailAddress(mailAddress)) » ;

- si l'utilisateur existe bien, le code appelle la méthode « SecurityManager.GeneratePassword() » ;

- cette méthode va hasher le mot de passe et l'assigner à la propriété « Password » de l'entité précédemment remontée de la base de données ;

- puis l'entité est mise à jour en base ;

- si cette mise à jour réussie, un email de confirmation est envoyé à l'internaute ;

- finalement, l'entité est à jour dans le cache.

Si le code nécessaire à la réalisation de ces opérations se trouve dans la page Web, il est perdu. S'il est placé dans la BLL, il peut tout à fait être réutilisé dans une application Windows ou Console.

La méthode ''GenerateNewPassword'' accessible dans la couche métier.

/// <summary>
/// Generate a new password, update database, send an email to confirm
/// </summary>
/// <param name="email">User email to query database</param>
/// <param name="culture">Culture to send a localized confirmation email</param>
/// <returns>True if success, otherwise false</returns>
public static bool GenerateNewPassword(string email, CultureInfo culture)
{
// Create an email instance to use proper load method.
// This also ensure email string is correctly formatted
MailAddress mail = null;
try
{
mail = new MailAddress(email);
}
catch { return false; }

// Load user entity from email address
UserEntity user = UserManager.Load(mail);
// If user account exists
if (user != null)
{
// Generate new password randomly
string pass = SecurityManager.GeneratePassword(ParametersManager.PassLength);
// Hash password to update entity
user.Password = SecurityManager.Md5GetHash(pass);
// If database update sucessful
if (UserManager.Update(user))
{
// Send confirmation email
SendChangePasswordEmail(user, pass, culture);
// Update cache
UpdateUsersDico(user);
return true;
}
}
else
{
return false;
}
return false;
}

IV-D - Maintenance en mémoire des données importantes

Autre exemple : afin de ne pas systématiquement envoyer des requêtes sur les sources de données, l'application maintien les résultats de certaines requêtes en cache. Elles sont stockées dans des variables statiques de type « Dictionary ». Avant donc de faire appel à la source de données, la BLL recherche dans les dictionnaires si l'objet recherché ne s'y trouve pas déjà. Dans ce cas, il est récupéré. C'est d'autant plus important que la source de données provient d'un tiers proposant un Service Web.

IV-E - Changement de la DAL

Voici une hypothèse simple. Mettons que nous ayons commencé à développer l'application avec comme source de données un fichier XML. La quantité de données augmentant, la performance de l'application n'est plus assurée et il faut migrer les données sous SQL Server. Grâce à l'architecture en couches, seule la DAL est affectée. Le code sur lequel il faut intervenir est facile à isoler et à modifier.

Au passage, voici une ré-implémentation de la méthode « LoadData ». Cette écriture utilise des interfaces. Cela permet de se rendre indépendant du système de gestion de base de données. Attention, la fabrique ne fournit pas d'objets pour tous les systèmes de gestion de base de données. A priori, seuls les espaces de noms suivants sont supportés par la fabrique :

- System.Data.EntityClient.EntityProviderFactory ;

- System.Data.Odbc.OdbcFactory ;

- System.Data.OleDb.OleDbFactory ;

- System.Data.OracleClient.OracleClientFactory ;

- System.Data.SqlClient.SqlClientFactory.

Le code de la méthode ''LoadData'' utilisant la fabrique ''DbProviderFactory''

public class PersonProvider
{
public List<PersonEntity> LoadData()
{
List<PersonEntity> list = new List<PersonEntity>();

// Creation de la fabrique
DbProviderFactory factory = DbProviderFactories.GetFactory(
ConfigurationManager.ConnectionStrings["ChaineDeConnexion"].ProviderName);
// Objet connection
using (IDbConnection cn = factory.CreateConnection())
{
cn.ConnectionString = ConfigurationManager.ConnectionStrings["ChaineDeConnexion"].ConnectionString;
cn.Open();

using (IDbCommand cmd = factory.CreateCommand())
{
cmd.CommandText = "Select * from [Persons]";
cmd.Connection = cn;

using (IDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
PersonEntity p = new PersonEntity();
p.Id = rdr["PersonId"] == DBNull.Value ? string.Empty : rdr["PersonId"].ToString();
p.Nom = rdr["LastName"] == DBNull.Value ? string.Empty : rdr["LastName"].ToString();
p.Prenom = rdr["FirstName"] == DBNull.Value ? string.Empty : rdr["FirstName"].ToString();

list.Add(p);
}
}
}
}

return list;
}
}

V - Conclusion

J'espère que cet article aura su vous montrer l'intérêt de l'architecture multicouche. Nous sommes partis d'une application Web avec un indice de performance de 61 et sommes remontés à 94 en gagnant en lisibilité et stabilité.

Grâce à cette architecture votre code sera plus facile à comprendre, à maintenir et à faire évoluer. La factorisation du code sous cette forme permet de réutiliser vos objets pour des applications d'autres types (consoles, Windows). En effet, le propre de la BLL est de ne pas être spécialisé pour une interface en particulier.

Il y a de nombreux aspects qui pourraient être traités : l'utilisation des interfaces associées aux fabriques notamment. Ce sera certainement dans un prochain article.

VI - Références

- Hosting WCF Services in IIS, de Cliff Simpkins

- Design Fundamentals

- Multitier architecture

- Architecture logicielle

- Architecture conception multicouche

Remerciements

Merci à Philippe Vialatte qui me pousse à me dépasser, Skyounet, tomlev. Merci à mes correcteurs auxquels je donne beaucoup de travail : Mahefasoa et jacques_jean.