|

Écrivez
à AILES ! |

Retour
vers programmation |

Retour
vers les questions Objet

Retour vers les
questions sur le
Polymorphisme
|
|
|
|
Questionnaire Objet
Polymorphisme
[javaworld],
[schupp],
[histoiryLge], [eiffel]
|
On retrouve le polymorphisme sous 4
formes différentes, mais une seule d'entre elle intervient
dans le polymorphisme couramment manipulé dans un langage
objet : le polymorphisme dynamique universel d'inclusion (ça
en jette, non ?)
En examinant l'utilisation du binding
que fait ce polymorphisme, on peut en
déduire ses avantages et mieux comprendre en quoi c'est une
des caractéristiques majeurs de la programmation objet. |
4 types de polymorphisme :
[javaworld] mentionne la classification de Luca
Caedelli et Peter Wegner, auteurs de "On Understanding Types,
Data Abstraction, and Polymorphisme".
[histoiryLge]
complète en rajoutant
que le même Wegner, dans "The Object-Oriented Classification
Paradigm" (1988) précise que le terme a été introduit par
Strachey en 1960.
D'une manière générale, le polymorphisme
représente la capacité d'une entité à posséder plusieurs formes.
Ici, "l'entité" en question est le
type.
Cette classification a donc été initiée par
Strachey et implique 2 catégories de polymorphismes, elles-même
divisées en 2 :
* "universal polymorphisme" :
fonctionne
sur un ensemble défini de types qui ont une structure commune ;
* "ad-hoc polymorphisme" :
fonctionne sur
un nombre limité de types qui n'ont pas de lien entre eux ;
Plus en détail, cela donne :
* "universal polymorphisme" : 2 mécanismes
permettent de définir un "ensemble défini de type possédant
une structure commune".
- l'un est à chercher dans le paradigme
de programmation générique;
- l'autre dans l'héritage et le
binding
de type.
=> "parametric
polymorphisme" : une seule abstraction s'applique à un ensemble
de type. Typiquement, List<T> (lien) peut s'appliquer à tout
type. Il suffit "d'instancier le template" pour une classe
donné (les connaisseurs en C++ apprécieront) pour créer une liste
"Liste<String>". Le lien précédent fait même
allusion à la généricité contrainte, où la structure commune des
types sur lesquels s'appliquent l'abstraction est un peu mieux
défini. Dans ce cas, l'ensemble défini de type est limité.
Rq : Java, bien sûr, ne supporte pas, avec ses List(Object) un tel
polymorphisme,
=> "inclusion
polymorphisme" (dit aussi "inheritance ou subtype
polymorphisme) : la structure commune est issue d'une phénomène
d'inclusion (un type est inclus dans un autre). Dans le cas des
langages objets, cette inclusion prend la forme d'un héritage ou d'un
"sous-typage", et l'ensemble défini de type est
virtuellement... sans limite.
* "ad-hoc polymorphism" : 2 mécanismes
permettent de fonctionner "sur un nombre limité de type non
liés entre eux" :
- l'un la conversion de type ;
- l'autre utilise la surcharge;
Comme on va le voir, ces variétés de
polymorphisme sont plus des facilités de programmation que de vrais
"polymorphismes génériques".
=> "coercion"
: conversion d'un type vers celui attendu par un opérateur ou une
fonction, ce qui permet d'éviter les erreurs de compilation. Son
expression la plus connue est le casting.
Ce casting est soit implicite, soit explicite.
> Il est fait automatiquement par le compilateur pour les types de
base (int en double, etc.) pour peu que l'opérateur ou la fonction
qui en a besoin est bien définie pour les types castés.
Ainsi, 1.0 + 1 marche parce que le langage Java défini un opérateur
+ acceptant 2 arguments de type "double" et que le langage
implicitement converti le deuxième argument "int" en
"double".
> Il est fait explicitement par le programmeur, pour peu que le
type casté fasse bien parti de la hiérarchie du type attendu (cf.
ex. plus bas).
=> "overloading"
: permet l'usage du même opérateur ou du même nom de méthode pour plusieurs significations distinctes de
programmation (cf. dynamic
binding ou dynamic dispatching).
Java supporte certains opérateurs surchargés, mais ne permet pas la
définition de ses propres opérateurs. Java supporte la surcharge des
noms de méthodes, pour peu que les signatures soient différentes.
Variable polymorphe ?
Une variable peut être polymorphe au sens coercion (ad-hoc) ou
inclusion (universal).
Exemple:
class stack{};
class counting_stack : public stack{
public : int size();
public : void push(int);
}
coercion :
stack s;
counting_stack cs;
s=cs ; // coercion de cs dans s : la zone mémoire allouée à s est
plus petite que celle de cs : tout un bout de cs "est
tronqué" et n'est pas recopié dans celle de s!
cs=s ; // illégal : la zone mémoire allouée à cs est plus grande que
celle de s... donc on ne sait pas quoi recopier dans la zone mémoire
propre à cs, à savoir celle qui contient la méthode size() !
inclusion :
stack * sp = new stack;
counting_stack* csp = new counting_stack;
sp=csp; // correct car le type de sp est parent de celui de csp. Aucune
zone mémoire n'est ici tronquée : les 2 variables pointent sur la
même adresse. Simplement, la table virtuelle
des fonctions sera moins
étendue via sp que via csp.
Le lien entre polymorphisme et binding
Il s'agit du dynamic binding, qui, dans le
cas des fonctions, devient le "dynamic dispatching" et qui
s'appuie sur le polymorphisme d'inclusion (de hiérarchie de type).
Si l'on reprend nos stack et counting_stack, pour illustrer le dynamic
binding (ou polymorphisme dynamique pour fonctions) :
stack* s=new stack;
counting_stack* cs=new counting_stack;
sp-> push(1);
csp-> push(2);
sp=csp; // sp, variable polymorphique
d'inclusion
sp->push(3); // quel push est appelé ?
2 réponses possibles, en fonction du moment
où le binding intervient :
- stack::push est appelé ; il s'agit d'un binding statique;
- counting_stack::push est appelé, le binding est donc dynamique.
On retrouve ce binding dans la réponse suivante.
Les fonctions virtuelles en C++ se basent sur le polymorphisme
d'inclusions et ne sont mises en œuvre que via des variables
polymorphes. cf. le mécanisme des tables virtuelles.
Polymorphic attachment
Avant de voir en quoi le polymorphisme intervient
dans les liaisons, il faut bien préciser ce que l'on relie.
D'un côté, vous avez une variable
qui possède un type.
De l'autre vous avez un objet,
instance d'une classe.
Lorsque vous assignez l'objet à la variable typée, vous
faites un binding
de valeur.
Ex : Base baseVar = new
Derived();
Attacher plusieurs références à un objet
Si l'on considère la hiérarchie de
classe suivante :
Derived2 qui hérite de Derived qui hérite de Base, on
peut associer à l'objet "new Derived2()" une variable de
type Derived ou Base.
L'effet dans ce cas est la réduction des
fonctionnalités de l'objet (elles ne sont plus accessibles
via l'interface associée au type de la variable). Mais tout appel aux
fonctionnalité de la variable de type Base exécutera pourtant dans
la bonne implémentation (celle de Derived2).
C'est le polymorphisme dynamique d'inclusion ou
"dynamic dispatching" dans la réponse précédente.
Un premier intérêt de ce type de polymorphisme
est donc de détacher le "quoi" (les fonctions de Base, qui
doivent être les seules disponibles via la variable de type Base), du
comment (l'implémentation de ces mêmes fonctions doit être celle de
l'objet, ici "new Derived2()").
Attacher une références à plusieurs objet
Si vous avez une classe qui possède la fonction void
f(Base aBase) et que vous lui passez des variables "d" de
type Derived, puis "d2" Derived2, vous associez
successivement au même objet ("aBase") plusieurs
références ("d" et "d2");
Si cette fonction f appelle une méthode de Base
(et elle ne peut faire autrement, vu son type), ce sera là encore la
bonne implémentation qui sera appelée, et ce quelque soit la
référence (Derived, Derived2, même Derived3 créée plus tard)
passée à l'objet aBase.
Un deuxième intérêt du polymorphisme dynamique
d'inclusion est donc de pouvoir appeler les bonnes implémentations de
classes... même si vous ne les avez pas encore écrite ! Tant
qu'elles dériveront de Base, ce polymorphisme fonctionnera
naturellement.
Cette distinction entre la variable d'un côté,
l'objet de l'autre permet de mieux aborder la différence qui existe
entre type et interface.
|