Le polymorphisme
Définition
D’un point de vue étymologique, le mot « polymorphisme » est constitué de deux mots :
-
« Poly » qui signifie « plusieurs »
-
« Morphisme » qui signifie « formes »
Autrement dit, appliqué aux membres d’une classe, le polymorphisme permet de manipuler différents types d'objets à travers une même classe de base ou une même interface, tout en exécutant un comportement adapté au type réel de l'objet. En C#, seuls les méthodes et accesseurs peuvent être polymorphes.
Le langage C# propose trois manières d’implémenter le polymorphisme :
-
Le polymorphisme par héritage en définissant des méthodes / accesseurs virtuels dans une classe de base, qu’il est possible de redéfinir dans les classes dérivant de cette classe pour remplacer ou compléter leur implémentation.
-
Le polymorphisme par abstraction en définissant des méthodes / accesseurs abstraits dans un classe de base, que nous devons redéfinir dans les classes dérivant de cette classe (à moins qu’elles-mêmes se déclarent abstraites pour déléguer de nouveau aux classes dérivées).
-
Le polymorphisme par interface en exposant les classes et leurs méthodes / accesseurs au travers d’interfaces qu’elles implémentent.
Dans le code, une caractéristique fondamentale du polymorphisme est de pouvoir appeler une méthode ayant une même signature, mais dont l’implémentation diffère selon le type réel de l’objet manipulé.
Le polymorphisme par héritage
Le polymorphisme par héritage consiste à définir des méthodes / accesseurs virtuels (avec le mot clé virtual) dans une classe, afin d’avoir la possibilité de les redéfinir dans les classes dérivées (avec le mot clé override). Pourquoi redéfinir une méthode ou un accesseur dans une classe dérivée (au choix) ?
-
Pour compléter le comportement de la méthode dans la classe de base (mode complétion)
-
Pour remplacer le comportement de la méthode dans la classe de base (mode annule et remplace)
Dans le diagramme de classes précédent, la méthode Frire est déclarée virtuelle dans la classe Gateau, et est redéfinie dans la classe Donut pour changer totalement son implémentation ou la compléter. La classe Beignet ne redéfinit pas cette méthode, car le comportement qu’elle propose dans la classe Gateau lui convient :
public class Gateau
{
// Autres membres ...
public virtual void Frire()
{
// Implémentation
}
}
public class Donut : Gateau
{
// Autres membres ...
public override void Frire()
{
// Implémentation
}
}
Le polymorphisme par abstraction
Le polymorphisme par abstraction consiste à définir des méthodes abstraites (avec le mot clé abstract) dans une classe, car même si ce service doit être proposé dans la classe, il est trop haut dans l’arbre d’héritage pour proposer une implémentation (un bloc de code). Les conséquences sont les suivantes :
-
L’obligation de définir la classe comme étant abstraite
-
L’obligation de redéfinir dans les classes héritées immédiates (avec le mot clé override) en conservant la même signature. Il y a donc une notion de « contrat ». Toutefois, une classe dérivée peut être déclarée comme étant aussi abstraite afin de déléguer de nouveau l’implémentation aux classes qui en dérivent
-
Impossibilité d’instancier la classe devenue abstraite
Dans notre diagramme de classes, nous définissons la méthode Frire() comme étant abstraite (avec le mot-clé abstract), car il n'est pas possible de proposer une implémentation dans la classe Gateau. En conséquences, la classe Gateau devient abstraite :
public abstract class Gateau
{
// Autres membres ...
public abstract void Frire();
}
public class Donut : Gateau
{
// Autres membres ...
public override void Frire()
{
// Implémentation
}
}
public class Beignet : Gateau
{
// Autres membres ...
public override void Frire()
{
// Implémentation
}
}
Et nous devons redéfinir cette méthode (avec le mot-clé override) dans les classes dérivées Donut et Beignet :
Le polymorphisme par interface
Le polymorphisme par interface consiste à manipuler différents objets au travers d’une interface commune qu’ils implémentent. Les classes concernées n’ont pas besoin d’appartenir au même arbre d’héritage : elles doivent respecter le contrat défini par l’interface.
Dans notre exemple, nous pourrions coder une interface nommée IPreparable proposant une méthode nommée Preparer(). La classe Pate implémenterait cette interface et fournirait sa propre implémentation de cette méthode. D’autres classes pourraient également implémenter cette interface, chacune proposant une implémentation différente (autrement dit plusieurs formes).
Ainsi, un traitement utilisant une référence de type IPreparable pourrait appeler la méthode Preparer() sans avoir à connaître le type concret de l’objet manipulé. L’implémentation exécutée dépendrait alors de la classe ayant été utilisée pour créer l’objet.
Voici un exemple :
public interface IPreparable
{
void Preparer();
}
public class Pate : IPreparable
{
// Autres membres ...
// Implémentation de la méthode de l'interface IPreparable
public void Preparer()
{
// Implémentation
}
}```
La méthode *Preparer* est **polymorphe**, car elle peut avoir une implémentation différente en fonction de la classe dans laquelle elle se situe.
Voici la classe *Patissier* qui propose une méthode nommée *PreparerGateau* :
```csharp
public class Patissier
{
// Autres membres ...
public void PreparerGateau(IPreparable element)
{
element.Preparer();
}
}
La méthode PreparerGateau ci-dessus peut prendre en paramètre n’importe quel objet dont la classe implémente l'interface IPreparable :