Créer ses propres classes génériques

Créer ses propres classes génériques

Introduction

Le framework .NET propose toute une panoplie de classes dites génériques.
L'exemple le plus connu est probablement les List<type> qui permettent 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.

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

En fait, 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, tout au long du tuto.

Contexte de base

Je travaille 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 dois créer (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 ? Telle est la question du jour...

Pour ce faire, il me faut penser de la façon suivante :

Quels sont les comportements similaires ?

Un véhicule peut :

Rouler,
Klaxonner,
Se comporter sur la route,
A partir ce 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 si nécessaire).

Définition de base de ma classe véhicule...


Notons la syntaxe suivante :

public class Vehicule <ClassChassis, ClassMoteur, ClassRoues>

Elle définit trois types manipulés par ma classe Vehicule...

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...
ClassChassis
ClassMoteur
ClassRoues

Les contraintes de type

Pour palier à cela, nous allons préciser les types génériques déclarés par des clauses de précision (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 juste après notre exemple,
Dans notre code cela donne :

Ci dessus, la contrainte nous informe que les types :

ClassChassis
ClassMoteur
ClassRoues
disposent d'un constructeur vide.

Cela me permet de pouvoir instancier dans le constructeur les instances de moteur, châssis et roues.

Les contraintes possibles sont les suivantes (D'après : 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 Gen<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.

Revenons à notre exemple, nous avons besoin de connaître les membres 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). Il m'est également possible de définir des classes qu'héritent mes classes (abstraites ou non), suivant le contexte.

Pour ce faire, et parce que je suis propre dans mes architectures, je vais créer un autre assembly, qui ne contient que les interfaces de mes objets. Dans ce dernier, je vais définit trois interfaces :

IMoteur,
IRoue,
IChassis,
Je vais me servir de ces définitions pour pouvoir manipuler mes types au sein de ma classe générique.

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>> IRoue
+ NomModele : String
+ Taille
+ Tourne (vitesse : Int32) : String

IChassis définit les méthodes et propriétés suivantes :

<<Interface>> IChassis
+ NomModele : String
+ Comportement (typeRoute : String) : String

La définition de ma classe prendra cette syntaxe :

Notons :

L'ajout d'une instruction using pointant vers l'assembly qui contient les interfaces,
L'ajout des contraintes forçant / définissant les interfaces implémentées par les types,
L'ordre des contraintes : la directive « constructeur vide » (new ( ) ) est toujours la dernière.

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.

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

Exemples de syntaxe :

Classe déclarée et instanciée :

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

Classe dérivée :

public class VehiculeUtilitaire : 
    Vehicule<ChassisUtilitaire,MoteurSimple,RoueJanteAcier>

{    }

Exemple réel

Il m'a été demandé un jour de concevoir un mécanisme de DAL (data access layer), mais qui permet 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.

En gros 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é de 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 :
Le type de l'objet à manipuler,
Le type qui permet de disposer des éléments critères de recherche
Le type représentant une liste d'objet.

Cela fera l'objet d'un autre tuto...

Remarque

L'exemple est implémenté dans l'archive auto extractable http://codes-sources.commentcamarche.net/source/49513-exemple-lie-au-tuto-creer-ses-propres-classes-generiques

Elle contient 4 Assembly (projets)

BDV.Exemple.Generique: Contient une application Winform exécutant l'exemple,
BDV.Exemple.Generique.Classes: Contient la classe générique
BDV.Exemple.Generique.Classes.Metier: Contient les classes métier (Roue, moteur, châssis)
BDV.Exemple.Generique.Interfaces: Contient les Interfaces (IRoue, IMoteur, IChassis)

Ce document intitulé « Créer ses propres classes génériques » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.