Aller au contenu

Le polymorphisme

Définition

D’un point de vue étymologique, le mot « polymorphisme » est constitué de deux éléments :

  • « Poly » qui signifie « plusieurs »

  • « Morphisme », provenant de « morphê » 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#, seules les méthodes et les propriétés (plus les indexeurs et évènements) peuvent être polymorphes.

Le langage C# propose trois manières d’implémenter le polymorphisme :

  • Le polymorphisme par héritage, qui définit des méthodes / propriétés virtuelles 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, qui définit des méthodes / propriétés abstraites dans une classe de base, que nous devons redéfinir dans les classes dérivant de cette classe (à moins qu’elles ne soient elles-mêmes déclarées abstraites, afin de déléguer leur implémentation aux classes qui en dérivent).

  • Le polymorphisme par interface, qui consiste à manipuler des objets au travers d’interfaces implémentées par leurs classes.

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 / propriétés virtuelles (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). Une méthode ou une propriété peut être redéfinie dans une classe dérivée pour l’une des deux raisons suivantes :

  • Compléter le comportement de la méthode dans la classe de base (mode complétion)

  • 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 l’implémentation proposée par sa classe de base 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. Le service doit être exposé par cette classe, mais elle se situe à un niveau trop élevé dans l’arbre d’héritage pour pouvoir en proposer une implémentation. Les conséquences sont les suivantes :

  • L'obligation de définir la classe comme étant abstraite

  • L'obligation de redéfinir ces méthodes dans les classes dérivées directes (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 aussi être déclarée comme étant abstraite afin de déléguer de nouveau l’implémentation aux classes qui en dérivent.

  • L'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équence, la classe Gateau devient abstraite :

public abstract class Gateau
{
    // Autres membres ...

    public abstract void Frire();
}

Et nous devons redéfinir cette méthode (avec le mot-clé override) dans les classes dérivées Donut et Beignet :

public class Donut : Gateau
{
    // Autres membres ...

    public override void Frire()
    {
        // Implémentation
    }
}

public class Beignet : Gateau
{
    // Autres membres ...

    public override void Frire()
    {
        // Implémentation
    }
}

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. Cette implémentation dépend du type réel de l’objet référencé par element. Voici la classe Patissier qui propose une méthode nommée PreparerGateau :

public class Patissier
{
    // Autres membres ...

    public void PreparerGateau(IPreparable element)
    {
        element.Preparer();
    }
}

La méthode PreparerGateau peut recevoir en paramètre n’importe quel objet créé à partir d'une classe qui implémente l'interface IPreparable :

var patissier = new Patissier();
var pate = new Pate();

patissier.PreparerGateau(pate);