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 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 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 :
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 :
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 :
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 :
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ée 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 :
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 :
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 :
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 :
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 :
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 :
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 » :
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 :
<%@ 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>
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 :
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 ».
[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 :
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 :
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 ».
<%@ 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 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éussit, 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.
///
<
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.
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▲
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.