Créer ses propres classes génériques

DE VUYST Benjamin

Le framework .NET propose toute une panoplie de classes dites génériques.
L'exemple le plus connu est probablement les List<type> qui permet de créer des listes fortement typées.
Ce mécanisme est puissant, et nous pouvons nous aussi définir des classes génériques dans nos développements applicatifs.

Mais qu'est ce qu'une classe générique ?

- Titre : Créer ses propres classes génériques

- Auteur : DE VUYST Benjamin

- Parution : 18 mai 2009

- Licence :

Sommaire

- I. Introduction

- II. Contexte de base

- III. Définition de ma classe (mon template) véhicule

- IV. Les contraintes de type

- IV-A. Utilisation d'interface pour préciser les contraintes de type

- IV-B. Exemple d'implémentation d'une méthode de la classe générique : Roule ( )

- IV-C. Utilisation de la classe générique

- V. Exemple réel

- VI. Remerciements

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 :

Déclaration de la classe Vehicule avec la clause Where de base

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.

La clause - where type : new() - permet l'instanciation du type contraint

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.

Classe Vehicule avec une méthode implémentée (Roule)

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.

Classe déclarée et instanciée

Classes.Vehicule<Classes.Metier.ChassisUtilitaire,
Classes.Metier.MoteurSimple,
Classes.Metier.RoueJanteAcier> vehicule =
new Classes.Vehicule<Classes.Metier.ChassisUtilitaire,
Classes.Metier.MoteurSimple,
Classes.Metier.RoueJanteAcier> ();

Classe dérivée

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.