I. Introduction▲
Le framework .NET propose toute une panoplie de classes dites génériques.
L'exemple le plus connu est probablement le type List<type> qui permet de créer une
liste fortement typée.
Ce mécanisme est puissant, et nous pouvons nous aussi définir des classes génériques dans le cadre
de nos développements applicatifs.
Mais qu'est ce qu'une classe générique ?
Une classe générique est un Template (modèle) de comportements que je peux appliquer à de multiples situations,
sur de multiples objets.
Pour expliquer cela, nous nous baserons sur un exemple simple, que nous utiliserons tout au long
de ce tuto.
[FTP]Sources : BDV.Exemple.Generique.zip
[HTTP](lien de secours) Sources : BDV.Exemple.Generique.zip
II. Contexte de base▲
Je travaille (pour l'exemple) dans une entreprise qui fabrique des véhicules à moteur. Véhicules pour lesquels toutes les associations sont possibles ! (tous les châssis peuvent accueillir tous les moteurs, et tous les modèles de roues).
Ajoutons que je suis fainéant, et que je n'ai pas envie de ré écrire l'entièreté de la définition d'un
véhicule à chaque modèle que je crée (ou chaque association possible).
Néanmoins, je dois contrôler, la définition des associations (moteur, châssis, roues).
Pourquoi ne pas créer un Template de voiture ? (comme par hasard...)
Pour ce faire, il me faut penser de la façon suivante :
Quels sont les comportements similaires aux différents véhicules ?
Un véhicule peut :
- Rouler,
- Klaxonner,
- Se comporter sur la route,
A partir de cela, je sais ce qui sera implémenté de façon commune,
ma classe générique se bornant à effectuer les appels aux types manipulés (appeler le moteur, les
roues et le châssis lorsque cela est nécessaire).
III. Définition de ma classe (mon template) véhicule▲
Notons la syntaxe suivante :
public class Vehicule <ClassChassis,
ClassMoteur, ClassRoues>
Cette syntaxe définit trois types manipulés au sein de ma
classe (ClassChassis, ClassMoteur, ClassRoues).
Rapidement deux problèmes se présentent :
- Une erreur du compilateur : je ne peux pas instancier de valeur dans le constructeur pour les types déclarés,
- Je ne connais pas les opérations accessibles pour les types définis, et donc, je ne peux pas utiliser leurs comportements respectifs,
IV. Les contraintes de type▲
Pour pallier à cela, nous allons préciser les types déclarés par des clauses (comme en SQL, lorsque
nous filtrons avec une clause where).
En .NET cette notion de précision se présente comme des contraintes de type.
Ces contraintes se déclarent juste après la déclaration de la classe, suivant le canevas suivant :
where <type> : contrainte
[,contrainte]
Nous verrons la liste des contraintes possibles plus tard,
dans notre code cela donne :
public
class
Vehicule<
ClassChassis,
ClassMoteur,
ClassRoues>
where
ClassChassis :
new
(
)
where
ClassMoteur :
new
(
)
where
ClassRoues :
new
(
)
Ci dessus, la contrainte nous informe que les types
o ClassChassis
o ClassMoteur
o ClassRoues
disposent d'un constructeur vide (" : new() ").
Cette contrainte (: new ()) me permet de pouvoir instancier dans le constructeur les instances de moteur, châssis et roues.
public
class
Vehicule<
ClassChassis,
ClassMoteur,
ClassRoues>
where
ClassChassis :
new
(
)
where
ClassMoteur :
new
(
)
where
ClassRoues :
new
(
)
{
private
ClassChassis m_chassis;
private
ClassMoteur m_moteur;
private
ClassRoues m_modeleRoue;
public
Vehicule
(
)
{
// Permis -> contrainte where new()
this
.
m_chassis =
new
ClassChassis
(
);
this
.
m_moteur =
new
ClassMoteur
(
);
this
.
m_modeleRoue =
new
ClassRoues
(
);
}
Les contraintes applicables sur les classes sont les suivantes
(source : MSDN Contraintes
de type (MSDN http://msdn.microsoft.com/fr-fr/library/d5x73970(VS.80).aspx ))
where T : struct |
L'argument de type T doit être un type valeur, non nullable (System.Int32, ... ou une structure définie dans le code), le type System.String n'est pas admis car il peut prendre null comme valeur. |
where T : class | L'argument de type T doit être un type référence, une classe. |
where T : new() |
L'argument de type T doit avoir un constructeur vide, si plusieurs contraintes sont définies,
celle ci doit être la dernière. |
where T : <className> |
L'argument de type T doit être de type <ClassName>, ou en dériver, où <ClassName> est également un type générique. Exemple : public class Sample <T1, T1parent> where T1 : T1parent { } |
where T : interfaceName | L'argument de type T doit implémenter l'interface interfaceName. |
where T : ClassName | L'argument de type T doit être de type ClassName, ou en dériver. |
IV-A. Utilisation d'interface pour préciser les contraintes de type▲
Revenons à notre exemple, nous avons besoin de connaître les membres de Moteur, Chassis et Roue
manipulables dans notre classe générique. Dès lors, il nous faut définir des Interfaces qu'implémentent
mes classes (Roues, Moteur et Châssis).
Pour ce faire, et parce que je suis propre dans mes architectures, je vais créer un autre assembly.
Cet assembly ne contiendra que les interfaces de mes objets, à savoir :
o IChassis
o IMoteur
o IRoues
Ces interfaces vont permettre à la classe générique de savoir ce qu'elle peut réaliser avec les
types définis.
IChassis définit les méthodes et propriétés suivantes :
<<Interface>> IChassis |
+ NomModele : String |
+ Comportement (typeRoute : String) : String |
IMoteur définit les méthodes et propriétés suivantes :
<<Interface>> IMoteur |
+ Cylindree : Int32 + Puissance : Int32 + Couple : Int32 + RapportEngagé : Int32 |
+ MonteRegime ( ) : String + ChangeRapportBoite (monteDescend : bool) : String |
IRoue définit les méthodes et propriétés suivantes :
<<Interface>> IRoues |
+ NomModele : String + Taille : Int32 |
+ Tourne (vitesse : Int32) : String |
La définition de ma classe prendra alors cette syntaxe :
using
System;
using
System.
Collections.
Generic;
using
System.
Text;
using
BDV.
Exemple.
Generique.
Interfaces;
// Permet la contrainte sur les Interface
namespace
BDV.
Exemple.
Generique.
Classes
{
public
class
Vehicule<
ClassChassis,
ClassMoteur,
ClassRoues>
where
ClassChassis :
IChassis,
new
(
)
where
ClassMoteur :
IMoteur,
new
(
)
where
ClassRoues :
IRoue,
new
(
)
Notons :
o L'ajout d'une instruction using pointant vers l'assembly qui
contient les interfaces,
o L'ajout des contraintes forçant / définissant les interfaces
implémentées par les types,
- IChassis
- IMoteur
- IRoue
o L'ordre des contraintes : la directive " constructeur vide
" (new ( ) ) est toujours la dernière.
IV-B. Exemple d'implémentation d'une méthode de la classe générique : Roule ( )▲
Dès lors que je connais les membres manipulés (par leur Interface), je peux implémenter de façon générique les comportements de ma classe véhicule.
public
class
Vehicule<
ClassChassis,
ClassMoteur,
ClassRoues>
where
ClassChassis :
IChassis,
new
(
)
where
ClassMoteur :
IMoteur,
new
(
)
where
ClassRoues :
IRoue,
new
(
)
{
private
ClassChassis m_chassis;
private
ClassMoteur m_moteur;
private
ClassRoues m_modeleRoue;
public
Vehicule
(
)
{
// Permis -> contrainte where new()
this
.
m_chassis =
new
ClassChassis
(
);
this
.
m_moteur =
new
ClassMoteur
(
);
this
.
m_modeleRoue =
new
ClassRoues
(
);
}
public
String Roule
(
)
{
StringBuilder strB =
new
StringBuilder
(
"["
+
System.
DateTime.
Now.
ToLongDateString
(
) +
"][ROULE]"
);
// Propriété connue -> Contrainte ClassMoteur:IMoteur
if
(
this
.
m_moteur.
RapportEngagé <=
0
)
strB.
AppendLine
(
this
.
m_moteur.
ChangeRapportBoite
(
true
));
// true monte le rapport, false le descend)
strB.
AppendLine
(
this
.
m_moteur.
MonteRegime
(
));
strB.
AppendLine
(
this
.
m_modeleRoue.
Tourne
(
this
.
m_moteur.
VitesseRotation));
return
strB.
ToString
(
);
}
public
String Klaxonne
(
)
{
throw
new
NotImplementedException
(
"Klaxonne non implémenté"
);
}
public
String ComportementSurRoute
(
)
{
throw
new
NotImplementedException
(
"Comportement sur route non implémenté"
);
}
}
IV-C. Utilisation de la classe générique▲
Il ne me reste plus qu'à déclarer des classes qui implémentent mes interfaces, et à instancier une classe Vehicule (ou une classe qui hérite d'un véhicule) pour pouvoir l'utiliser.
Classes.
Vehicule<
Classes.
Metier.
ChassisUtilitaire,
Classes.
Metier.
MoteurSimple,
Classes.
Metier.
RoueJanteAcier>
vehicule =
new
Classes.
Vehicule<
Classes.
Metier.
ChassisUtilitaire,
Classes.
Metier.
MoteurSimple,
Classes.
Metier.
RoueJanteAcier>
(
);
public
class
VehiculeUtilitaire :
Vehicule<
ChassisUtilitaire,
MoteurSimple,
RoueJanteAcier>
{
}
V. Exemple réel▲
Il m'a été demandé un jour de concevoir un mécanisme de DAL (data access layer), qui permette de ne plus
rédiger une seule ligne de code,
tout en disposant :du mécanisme d'accès aux données et de mapping relationnel objet.
Grosso modo ce mécanisme générique permet d'automatiser la génération des requêtes vers une base de données
SQL CE pour les opérations suivantes :
- Création de la table,
- Insertion de données,
- Update de données,
- Select all
- Select en fonction de critères (gérés dynamiquement).
Une sorte de framework comme Ibatis mais sans la complexité du paramétrage XML.
Son mécanisme repose sur deux principes :
- Décoration des classes à manipuler par des System.Attributes
" maison ",
La classe générique peut ainsi se baser sur ces attributes
pour disposer des informations permettant de
manipuler la base de données en lien avec les classes,
- Les types à spécifier pour la classe générique sont :
o Le type de l'objet à manipuler,
o Le type qui permet de disposer
des éléments critères de recherche
o Le type représentant une liste
d'objet.
Cela fera l'objet d'un autre tuto...
VI. Remerciements▲
Je remercie l'équipe dotnet pour la relecture, LG pour son efficacité et Adeline pour ses nombreuses corrections syntaxiques.