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 :

maison-numerobis.png

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 CRUDCreate Read Update Delete (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 Aïe avec ce schéma :

L'architecture multicouche

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 :

image

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

image

Visual Studio nous livre les résultats suivants :

image
Image personnelle 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''
Sélectionnez
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.

image
Image personnelle 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''
Sélectionnez
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 ?

image
Image personnelle 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''
Sélectionnez
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''
Sélectionnez
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 :

image

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

image
Image personnelle 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.
Sélectionnez
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.

image
Image personnelle 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''
Sélectionnez
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 :

image

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.
Sélectionnez
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''
Sélectionnez
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.

image
Image personnelle 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''
Sélectionnez
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''
Sélectionnez
public static class PersonManager
{
    public static List<PersonEntity> LoadData()
    {
        return new PersonProvider().LoadData();
    }
}

L'état de la solution est :

image

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 :

image

En mode Release :

image

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. Ouf 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
Sélectionnez
public partial class Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        GridView1.DataSource = PersonManager.LoadData();
        GridView1.DataBind();
    }
}
image
image

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
Sélectionnez
<%@ 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é
Sélectionnez
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.

image
Image personnelle 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.
Sélectionnez

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();
        }
    }
}
image

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 Fou ?

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 :

image

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.

image

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
Sélectionnez
[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''
Sélectionnez
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''
Sélectionnez
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 :

image

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

image

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''
Sélectionnez
<%@ ServiceHost Language="C#" Debug="true" Service="WcfServiceLibrary.ThreeTierServiceLibrary" %>
image

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:

image

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.

image

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

IV-C. Ajouter une gestion de mots de passe

J'ai dans mon projet « VideoCrossingVideoCrossing » 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.
Sélectionnez
/// <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''
Sélectionnez
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

Remerciements

Image personnelle Merci à Philippe VialattePhilippe Vialatte qui me pousse à me dépasser, SkyounetSkyounet, tomlevtomlev. Merci à mes correcteurs auxquels je donne beaucoup de travail : MahefasoaMahefasoa et jacques_jeanjacques_jean.