Polymorphisme (informatique)
En informatique et en théorie des types, le polymorphisme, du grec ancien polús (plusieurs) et morphê (forme), est le concept consistant à fournir une interface unique à des entités pouvant avoir différents types. Par exemple, des opérations telles que la multiplication peuvent ainsi être étendues à des scalaires aux vecteurs ou aux matrices, l'addition, des scalaires aux fonctions ou aux chaînes de caractères, etc. Il existe plusieurs sortes de polymorphismes fondamentalement différents :
Sorte | Nom en programmation orientée objet | Nom en programmation fonctionnelle | Description |
---|---|---|---|
polymorphisme ad hoc | surcharge | Une interface unique est implémentée par plusieurs routines ayant le même identifiant, potentiellement hétérogènes, c'est-à-dire chaque routine ayant une combinaison différente de types pour ses paramètres, qu'elle soit membre d'une classe ou non (routine globale). | |
polymorphisme paramétré | programmation générique | polymorphisme | Une interface unique est implémentée par une seule routine utilisant un type générique pour au moins un de ses paramètres, que la routine soit membre d'une classe ou non (routine globale). |
polymorphisme par sous-typage (héritage) | polymorphisme par héritage | polymorphisme d'inclusion | Une interface unique est implémentée par une routine membre ayant le même identifiant dans chaque classe faisant partie de la même hiérarchie d'héritage. |
Selon le langage informatique employé, le polymorphisme peut être réalisé par différents moyens, inhérents au langage ou par emploi de patrons de conception.
Polymorphisme ad hoc
[modifier | modifier le code]Le polymorphisme ad hoc ou surcharge de fonction consiste à déclarer plusieurs fonctions ou méthodes ayant le même nom, mais des paramètres différents (par leur nombre ou leurs types). Selon que le langage est typé statiquement ou dynamiquement, la détermination de la bonne implémentation pour les paramètres donnés se fait à la compilation ou à l'exécution. Voici un exemple de surcharge en C++ :
#include <string>
#include <iostream>
void fonctionSurchargee(std::string str)
{
std::cout << "Implémentation 1 (string)" << std::endl;
}
void fonctionSurchargee(int x)
{
std::cout << "Implémentation 2 (int)" << std::endl;
}
int main(int argc, char *argv[])
{
fonctionSurchargee("Hello World!"); // Affiche : Implémentation 1 (string)
fonctionSurchargee(42); // Affiche : Implémentation 2 (int)
return 0;
}
Polymorphisme paramétré
[modifier | modifier le code]Le polymorphisme paramétré consiste à définir des fonctions qui peuvent être appliquées à des types paramétrés. Par exemple, il est possible de définir une même fonction concat permettant de concaténer deux listes quel que soit le type de données qu'elles contiennent. Dans ce cas, le type de la fonction concat est noté :
où α est un paramètre qui représente le type de données que contiennent les listes. α peut correspondre aux types entier, réel, chaîne de caractères, listeβ, etc.
Le polymorphisme paramétré est utilisé par les langages Caml, Haskell, PureScript et Scala. Par exemple, en Haskell, la fonction take
renvoie les n premiers éléments d'une liste. La spécification de la fonction renvoyée par l'interpréteur est :
take :: Int -> [a] -> [a]
Le type a est une variable de type qui correspond à n'importe quel type, c'est donc une fonction qui supporte le polymorphisme paramétré.
Plusieurs langages orientés objet implémentent le polymorphisme paramétré, comme Java avec les types génériques et C++ avec les templates. On pourrait exprimer la fonction concat dans ce dernier comme :
#include <list>
template<class a>
std::list<a> concat(std::list<a> list1, std::list<a> list2)
{
return list1.insert(list1.end(),list2.begin(),list2.end())
}
Polymorphisme par sous-typage (héritage)
[modifier | modifier le code]Création de types
[modifier | modifier le code]L'idée est de partir d'un type et de le modifier. Par exemple, on peut créer une classe de base, puis faire des classes dérivées.
Ce concept est associé à l'approche orientée objet.
En C++
[modifier | modifier le code]#include <numbers>
class Forme {
public:
// Méthode virtuelle pure
virtual float Aire() = 0;
// Une classe de base d’une hiérarchie polymorphe
// doit toujours (ou presque) avoir un destructeur virtuel
virtual ~Forme() {}
};
class Carre : public Forme {
public:
float Aire() override { return m_cote * m_cote; }
private:
float m_cote;
};
class Cercle : public Forme {
public:
float Aire() override { return std::numbers::pi<float> * m_rayon * m_rayon; }
private:
float m_rayon;
};
En Java
[modifier | modifier le code]public abstract class Forme {
public abstract float aire() ;
}
public class Carre extends Forme {
private float cote;
@override
public float aire() {
return cote * cote;
}
}
public class Cercle extends Forme {
private float rayon;
@override
public float aire() {
return (float) Math.PI*rayon*rayon;
}
}
En Python
[modifier | modifier le code]import math
class Forme:
def aire(self):
raise NotImplementedError()
class Carre(Forme):
def __init__(self, cote):
self.cote = cote
def aire(self):
return self.cote * self.cote
class Cercle(Forme):
def __init__(self, rayon):
self.rayon = rayon
def aire(self):
return math.pi * self.rayon * self.rayon
En C#
[modifier | modifier le code]public abstract class Forme
{
public abstract float Aire();
}
public class Carre : Forme
{
private float cote;
public override float Aire()
{
return (float) Math.Pow(cote, 2);
}
}
public class Cercle : Forme
{
private float rayon;
public override float Aire()
{
return (float) ( Math.PI * Math.Pow(rayon, 2) );
}
}
En Eiffel
[modifier | modifier le code]deferred class
FORME
feature
aire: REAL
deferred end
end
class
CARRE
inherit
FORME
feature
cote: REAL
aire: REAL
do
Result := cote^2
end
end
class
CERCLE
inherit
FORME
MATH
feature
rayon: REAL
aire: REAL
do
Result := Pi * rayon^2
end
end
Utilisation des sous-types
[modifier | modifier le code]Grâce aux fonctions virtuelles, on peut faire un algorithme en n'utilisant que la classe de base qui va automatiquement appeler les fonctions des classes dérivées. On peut dire tout simplement qu'une liste polymorphe contient des objets différents, et selon le type de ces objets quand une méthode est utilisée, elle va être appliquée, à un des objets de cette liste, selon sa fonction.
En C++
[modifier | modifier le code]template<typename Size>
float AireTotal(std::array<Forme*, Size> arr) {
float f = 0;
for (auto forme : arr)
f+= forme->Aire(); // le programme détermine automatiquement quelle fonction appeler
return f;
}
// ...
std::array<Forme*, 3> tableau { new Carre, new Cercle, new Carre };
AireTotal(tableau);
// ...
En Java
[modifier | modifier le code]float aireTotal(Forme[] tabl) {
float s=0;
for(Forme forme : tabl) {
s += forme.aire(); // le programme détermine automatiquement quelle fonction appeler
}
return s;
}
// ...
Forme[] tableau = { new Carre(), new Cercle(), new Carre() };
aireT = aireTotal(tableau); //aireT aura été défini comme float
// ...
En C#
[modifier | modifier le code]//...
private float _aireT;
readonly Forme[] _tableau = { new Carre(), new Cercle(), new Carre() };
//...
float AireTotal(Forme[] tabl)
{
float s = 0;
foreach (Forme form in tabl)
{
s += form.Aire(); // le programme détermine automatiquement quelle fonction appeler
}
return s;
}
//...
_aireT = AireTotal(_tableau);
//...
En Eiffel
[modifier | modifier le code] aireTotal (tab: ARRAY [FORME]): REAL
do
-- Result = 0.0 par défaut
from
tab.start
until
tab.after
loop
Result := Result + tab.item.aire
tab.forth
end
end
Intérêt du polymorphisme
[modifier | modifier le code]En proposant d'utiliser un même nom de méthode pour plusieurs types d'objets différents, le polymorphisme permet une programmation beaucoup plus générique. Le développeur n'a pas à savoir, lorsqu'il programme une méthode, le type précis de l'objet sur lequel la méthode va s'appliquer. Il lui suffit de savoir que cet objet implémentera la méthode.
Ainsi, un logiciel de calcul d'intérêt pour des comptes bancaires se présenterait de la façon suivante en programmation classique (pseudo code) :
case MonCompteBancaire PEA : MonCompteBancaire.calculeInteretPEA PEL : MonCompteBancaire.calculeInteretPEL LivretA : MonCompteBancaire.calculeInteretLivretA end case
Si un nouveau type de compte bancaire PERP apparait (et avec lui un nouveau calcul), il sera nécessaire d'une part d'écrire la nouvelle méthode calculeInteretPERP, mais aussi de modifier tous les appels du calcul ci-dessus. Dans le meilleur des cas, celui-ci sera isolé et mutualisé de sorte qu'une seule modification sera nécessaire. Dans le pire des cas, il peut y avoir des centaines d'appels à modifier.
Avec le polymorphisme, toutes les méthodes porteront le même nom « calculeInteret » mais auront des codes différents (un par type de compte).
L'appel sera de la forme :
MonCompteBancaire.calculeInteret
Lors de l'arrivée du nouveau compte, aucune modification de ce code ne sera nécessaire. Le choix de la méthode réelle à utiliser sera fait automatiquement à l'exécution par le langage, alors que dans le cas précédent, c'est le développeur qui devait programmer ce choix. Mais le programmeur devra créer une classe objet supplémentaire pour implémenter le code correspondant qui sera appelé par une attribution de la table des méthodes virtuelles qui elle effectuera bien un case en fonction de ce composant.