Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici

La généricité

Des chiens et des chats

La généricité, présente dans de nombreux langages de programmation, permet d’aller plus loin dans l’industrialisation du code et de limiter la duplication de code entre plusieurs classes.

Nous allons aborder la généricité par le biais d’un problème difficile à régler sans faire appel à elle.

Soit une classe de données Dog permettant de représenter un chien. Cette classe, très simple, contient un unique attribut permettant de nommer l’animal :

data class Dog(val name: String) 

Ajoutons à ce programme une classe DogKennel représentant un chenil capable d’accueillir dix chiens :

class DogKennel 
{ 
 
 companion object 
 { 
 
   private const val KENNEL_CAPACITY = 10 
 
 } 
 
 private val dogs = arrayOfNulls<Dog>(DogKennel.KENNEL_CAPACITY) 
 
 var dogsCounter = 0 
   private set 
 
 private fun getAvailablePlacesNumber(): Int = 
     DogKennel.KENNEL_CAPACITY - dogsCounter 
 
 fun addDog(dog: Dog): Boolean 
 { 
   return if (getAvailablePlacesNumber() < 0) 
   { 
     false 
   } 
   else 
   { ...

Écrire une classe générique

L’idée, quand on écrit une classe générique, c’est d’ajouter, au moment de la déclaration de la classe, ce qu’on appelle un paramètre de type. Ce paramètre s’écrit entre chevrons :

class Kennel<T> 
{ 
} 

Ici, le paramètre de type est nommé T, c’est souvent une convention. Mais il est tout à fait possible de lui donner un tout autre nom. Par exemple : TheClassTypeParam, etc.

Dans la classe Kennel, il convient de remplacer partout l’utilisation de la classe Animal par ce fameux paramètre de type T :

class Kennel<T> 
{ 
 
 companion object 
 { 
 
   private const val KENNEL_CAPACITY = 10 
 
 } 
 
 private val animals = arrayOfNulls<Any>(Kennel.KENNEL_CAPACITY) 
 
 var animalsCounter = 0 
   private set 
 
 private fun getAvailablePlacesNumber(): Int = 
     Kennel.KENNEL_CAPACITY - animalsCounter 
 
 fun addAnimal(animal: T): Boolean 
 { 
   return if (getAvailablePlacesNumber() < 0) 
   { 
     false 
   } 
   else ...

Utiliser une classe générique

Maintenant que la classe Kennel est une classe générique, nous pouvons l’utiliser en indiquant le paramètre de type choisi au moment de l’instanciation. Pour indiquer le paramètre de type choisi, on recourt à la syntaxe qui vaut pour les tableaux.

Instancions un chenil qui ne contient que des chiens :

val dogKennel = Kennel<Dog>() 

Le programme précédent devient donc :

fun main() 
{ 
 val dogKennel = Kennel<Dog>() 
 val catKennel = Kennel<Cat>() 
 
 dogKennel.addAnimal(Dog("Doggo")) 
 catKennel.addAnimal(Cat("Kitty")) 
} 

Il n’est plus possible d’ajouter un chien dans le chenil des chats et un chat dans le chenil des chiens.

Ajouter une contrainte sur le type de paramètre

Le programme que nous venons d’écrire présente encore une petite faille.

Créons un nouvel objet, telle une classe :

data class Chair(val color: String) 

Dans le programme principal, rien ne nous empêche de créer un chenil de chaises :

val chairKennel = Kennel<Chair>() 

Ce programme n’a pas vraiment de sens. En tant que créateur du projet, nous ne risquons pas de l’écrire. Mais si plusieurs développeurs interviennent sur le code source, ils n’auront pas forcément en tête le contexte de création des classes et pourront les utiliser de manière incohérente. C’est pourquoi, quand on écrit du code, il faut être le plus expressif possible et mettre en place autant de gardes fous que possible. Dans le cas présent, nous aimerions que le chenil ne puisse contenir que des animaux, tout en gardant la généricité mise en place.

La solution consiste à indiquer que le type de paramètre T doit être un animal, via la syntaxe valant pour l’expression de l’héritage :

class Kennel<T: Animal> 
{ 
 //... 
} 

Le reste de la classe reste strictement identique. Il n’est maintenant plus possible de créer un chenil pour des chaises. Bien évidemment, rien n’empêchera un...

Ajouter plusieurs contraintes sur le type de paramètre

Si la modification précédente permet d’éviter la création d’un chenil de chaises, il est toujours possible de mélanger des chats et des chiens dans le même chenil. En effet, nous avons indiqué ici que le type de paramètre T doit être un animal, mais le type Animal est inclus. Par conséquent, rien de nous empêche de créer un chenil contenant des animaux via la syntaxe suivante :

val animalKennel = Kennel<Animal>() 

Puisque les classes Cat et Dog implémentent toutes les deux l’interface Animal, nous pouvons toujours ajouter ces deux types d’animaux dans le chenil.

Peut-on s’en sortir ? À chaque solution, un nouveau problème !

En programmation, il y a toujours moyen de s’en sortir. Ainsi, nous pouvons définir des règles métier supplémentaires sur le chenil. Exemple : tous les animaux ne sont pas éligibles à être accueillis dans un chenil (comme des lions, des tigres, des éléphants, etc.).

Imaginons une nouvelle interface que les animaux éligibles à l’accueil dans un chenil doivent implémenter :

interface Kennelable 

Modifions les classes Cat et Dog pour qu’elles implémentent cette nouvelle interface :

data class Dog(val name: String) ...

Les classes génériques covariantes

Le but d’une classe générique covariante est de pouvoir utiliser la classe générique avec un type dérivé du type générique. On peut par exemple déclarer une variable comme portant le type Animal tout en lui affectant un objet de type Dog, car la classe Dog implémente la classe Animal.

Pour mettre en place cette covariance, on écrit le mot-clé out devant le type générique. Ce qui donne dans la classe Kennel :

class Kennel<out T> 
{ 
 
 //... 
 
} 

Le programme suivant crée une variable de type Kennel<Animal>, qui est pourtant instanciée avec un objet de type Kennel<Dog> :

fun main() 
{ 
 val kennel: Kennel<Animal> = Kennel<Dog>() 
} 

Ce court programme compile parfaitement. Dès lors que l’on retire le mot-clé out devant le type générique de la classe Kennel, le programme ne compile plus et présente l’erreur suivante :

Type mismatch: inferred type is Kennel<Dog> but Kennel<Animal> 
was expected 

Afin de pouvoir utiliser les classes génériques covariantes, une restriction doit être respectée. Si la classe générique contient des méthodes, le type générique doit être utilisé uniquement...

Les classes génériques contravariantes

Le but d’une classe générique contravariante est de pouvoir utiliser la classe générique avec un type parent que le type générique. On peut par exemple déclarer une variable comme portant le type Dog tout en lui affectant un objet de type Animal, car la classe Dog implémente la classe Animal.

Pour mettre en place cette contravariance, on écrit le mot-clé in devant le type générique. Ce qui donne dans la classe Kennel :

class Kennel<in T> 
{ 
 
 //... 
 
} 

Le programme suivant crée une variable de type Kennel<Dog>, qui est pourtant instanciée avec un objet de type Kennel<Animal> :

fun main() 
{ 
 val kennel: Kennel<Dog> = Kennel<Animal>() 
} 

Une nouvelle fois, ce court programme compile parfaitement. Cependant, si l’on retire le mot-clé in devant le type générique de la classe Kennel, une erreur de compilation accompagnée du message suivant apparaît :

Type mismatch: inferred type is Kennel<Animal> but Kennel<Dog> 
was expected 

Afin de pouvoir utiliser les classes génériques contravariantes, une restriction doit être respectée. Si la classe générique contient des méthodes, le type générique...

En résumé

  • La généricité est très utilisée dans la programmation orientée objet et permet de réutiliser une classe avec plusieurs types.

  • Pour indiquer qu’une classe utilise la généricité, il convient de lui indiquer un type de paramètre, par exemple <T>.

  • Il est possible d’appliquer une ou plusieurs contraintes sur le type de paramètre. Si plusieurs contraintes doivent être écrites, le mot-clé where doit alors être utilisé.

  • Le mot-clé out permet de créer une classe générique covariante.

  • Le mot-clé in permet de créer une classe générique contravariante.