POLYMORPHISME d'OBJETS

Conversion de références d'objet entre classe et classe dérivée

 
Plan de ce chapitre:

Introduction

Instanciation et utilisation dans le même type

Instanciation et utilisation dans un type différent

Polymorphisme implicite

Instanciation dans un type descendant

Polymorphisme explicite par transtypage

Utilisation pratique du polymorphisme d'objet

instanciation dans un type ascendant
 

Polymorphisme (autres chapitres) :

par héritage de méthode

par héritage de classes abstraites

par implémentation d'interfaces

 

 
Introduction

Soit une classe Mere et une Fille héritant de la classe Mere :

Les objets peuvent avoir des comportements polymorphes (s'adapter et se comporter différement selon leur utilisation) licites et des comportements polymorphes dangereux selon les langages.

Dans un langage dont le modèle objet est la référence (un objet est un couple : référence, bloc mémoire) comme C++, C#, Delphi ou Java, il y a découplage entre les actions statiques du compilateur et les actions dynamiques du système d'exécution selon le langage utilisé le compilateur protège ou non statiquement des actions dynamiques sur les objets une fois créés. C'est la déclaration et l'utilisation des variables de références qui autorise ou non les actions licites grâce à la compilation.

Supposons que nous ayons déclaré deux variables de référence, l'une de classe Mere, l'autre de classe Fille, une question qui se pose est la suivante : au cours du programme quelle genre d'affectation et d'instanciation est-on autorisé à effectuer sur chacune de ces variables. L'héritage permet une variabilité entre variables d'objets de classes de la même hiérachie, c'est cette variabilité que dénommons le polymorphisme d'objet.

Nous allons dès lors envisager tous les situations possibles et les évaluer, les exemples appuyant le texte sont présentés en Delphi, lorsqu'il y a discordance avec l'un des 4 autres langages, celle-ci est mentionnée explicitement.

Remonter



instanciation dans le type initial et utilisation dans le même type
Il s'agit ici d'une utilisation la plus classique qui soit, dans laquelle une variable est utilisée dans son type de définition initial.

En Delphi :
var
   x,u : Mere;
   y,v : Fille ;
.....
x : = Mere.Create ; // instanciation dans le type initial
u := x; // affectation de références du même type
y : = Fille.Create ;  // instanciation dans le type initial
v := y;// affectation de références du même type

Remonter



instanciation dans le type initial et utilisation différente

Il s'agit ici de l'utilisation licite commune à tous les langages cités plus haut, nous illustrons le discours en explicitant deux champs de la classe Mere (chm:chaine et a:entier) et un champ supplémentaire (chf:chaine) dans la classe Fille. La figure ci-dessous indique les affectations possibles :

En Delphi :

var
  x , ObjM : Mere;
  y , ObjF : Fille;

ObjM := Mere.Create; // instanciation dans le type initial
ObjF := Fille.Create; // instanciation dans le type initial
 x := ObjM; // affectation de références du même type
 x := ObjF; // affectation de références du type descendant implicite
 y := ObjF; // affectation de références du même type
 y := Fille(ObjM); // affectation de références du type ascendant explicite mais dangereux si ObjM est uniquement Mere
 

Cette situation n°2  est typique de la notion du polymorphisme d'objet et nous amène à distinguer deux cas courant devant lesquels le programmeur va se trouver. Ces deux cas correspondent pour le premier à l'affectation de type implicite (x := ObjF; // affectation de références du type descendant implicite), pour le second il s'agit d'une affectation avec transtypage d'objet (y := Fille(ObjM); // affectation de références du type ascendant explicite).
 
 

Remonter



Polymorphisme d'objet implicite

Dans l'exemple précédent le compilateur accepte le transtypage  'y :=Fille(ObjM)' car il autorise un polymorphisme d'objet de classe ascendante vers une classe descendante (c'est à dire que ObjM peut se référer implicitement à tout objet de classe Mere ou de toute classe descendante de la classe Mere).
 
 

fig - 1


fig - 2

 
Dans la figure fig-1 ci-dessus, une hiérarchie de classes decendant toutes de la classe Mere, dans fig-2 ci-contre le schéma montre une référence de type Mere qui peut 'pointer' vers n'importe quel objet de classe descendante (polymorphisme d'objet).

Exemple pratique tiré du schéma précédent

Le polymorphisme d'objet est typiquement fait pour représenter des situations pratique figurée ci-dessous :

Une hiérarchie de classe de véhicules descendant toutes de la classe mère Vehicule,  on peut énoncer le fait qu'un véhicule peut être de plusieurs sortes : soit un croiseur, soit une voiture, soit un véhicule terrestre etc...

En termes informatiques, si l'on déclare une référence de type véhicule (var x : vehicule) elle pourra pointer vers n'importe quel objet d'une des classe filles de la classe vehicule.

D'une façon générale vous pourrez toujours écrire des affectations entre deux références d'objets :
var
 x : Classe1
 y : Classe2
..........
x := y;
si et seulement si Classe2 est une classe descendante de Classe1.
 
 

Remonter



instanciation dans un type descendant

Polymorphisme par création d'objet de classe descendante

Dans ce paragraphe nous signalons qu'il est tout à fait possible, du fait  transtypage implicite, de créer un objet de classe descendante référencé par une variable de classe parent.

Ajoutons 2 classes à la hiérarchie des véhicules :

Ensuite nous déclarons 3 références de type x:vehicule, y:voiture et z:break nous créons 3 objets de classe voiture, berline et break, il est possible decréer directement un objet de classe descendante à partir d'une référence de classe mère :


On pourrait reéécrire ces phrases afin de comprendre à quel genre de situation pratique cette opération correspond :


En Delphi cela donnerait le code suivant
var
 x : vehicule;
 y : voiture;
 z : break;
...
x := voiture.Create; // objet de classe enfant voiture référencé par x de classe parent vehicule
y := berline.Create; // objet de classe enfant berline référencé par x de classe parent voiture
z := break.Create; // instanciation dans le type initial
 
 

Remonter



Polymorphisme d'objet explicite par transtypage

Reprenons le code précédent en extrayant la partie qui nous interesse :
var
  x , ObjM : Mere;
  y , ObjF : Fille;

ObjM := Mere.Create; // instanciation dans le type initial
 x := ObjM; // affectation de références du même type
 y := Fille(ObjM); // affectation de références du type ascendant explicite licite

Nous avons signalé que l'affectation  y := Fille(ObjM) pouvait être dangereuse si ObjM pointe vers un objet purement de type Mere. Voyons ce qu'il en est.

Nous avons vu plus haut qu'une référence de type parent qui peut 'pointer' vers n'importe quel objet de classe descendante. Si l'on sait qu'une reférence x de classe parent, pointe vers un objet de classe enfant, on peut en toute sureté procéder à une affectation de cette reférence à une autre reférence y définie comme reférence classe enfant ( opération y := x ). Toutefois le compilateur refusera l'écriture y := x, il suffit de lui indiquer qu'il faut transtyper la variable de référence x et la considérer dans cette instruction comme une reférence sur un enfant

En Delphi :

var
 ObjM : Mere;
  y  : Fille;

ObjM := Mere.Create; // instanciation dans le type initial
 y := Fille(ObjM); // affectation de références du type ascendant explicite licite

La reférence ObjM est transtypée de telle manière que le compilateur puisse faire pointer y vers l'objet déjà pointé par ObjM. En reprenant l'exemple pratique de la hiérarchie des véhicules :

Puisque x pointe vers un objet de type voiture toute variable de référence acceptera de pointer vers cet objet, en particulier la variable voiture après transtypage de la référence de x.

En Delphi l'affectation s'écrirait par application de l'opérateur de transtypage :

y := voiture ( x );


 

ATTENTION
  • La validité du transtypage n'est pas vérifiée statiquement par le compilateur, donc si votre variable de référence pointe vers un objet qui n'a pas la même nature que l'opérateur de transtypage, c'est de l'exécution qu'il y aura production d'un message d'erreur indiquant le transtypage impossible.

  •  
  • Il est donc impératif de tester l'appartenance à la bonne classe de l'objet à transtyper avant de le transtyper, les langages C#, Delphi et Java dispose d'un opérateur permettant de tester cette appartenance ou plutôt l'appartenance à une hiérarchie.

 
Rappels sur l'opérateur  "is" en Delphi
 
L'opérateur is, qui effectue une vérification de type dynamique, est utilisé pour vérifier quelle est effectivement la classe d'un objet à l'exécution. 

L'expression :  objet   is   classeT

renvoie True si objet est une instance de la classe désignée par classeT ou de l'un de ses descendants, et False sinon. Si objet a la valeur nil, le résultat est False.
 

 

Remonter



Utilisation pratique du polymorphisme d'objet

Le polymorphisme d'objet associé au transtypage est très utile dans les paramètres des méthodes.

Lorsque vous déclarez une méthode P avec un paramètre formel de type ClasseT :

procedure P( x : ClasseT );
begin
    ........
end;

Vous pouvez utiliser lors de l'appel de la procédure P n'importe quel paramètre effectif de ClasseT ou bien d'une quelconque classe descendant de ClasseT et ensuite à l'intérieur de la procédure vous transtypez le paramètre. Cet aspect a été abondamment utilisé en Delphi lors de la création de gestionnaires d'événements communs à plusieurs objets :

procedure P1( Sender : Tobject );
begin
    if Sender is TEdit then
       TEdit(Sender).text := 'ok'
    else
      if Sender is TButton then
       TButton(Sender).caption := 'oui'
   ............
end;

Autre exemple avec une méthode P2 personnelle sur la hiérarchie des véhicules :

procedure P2( Sender : vehicule );
begin
     if Sender is voiture then
       voiture(Sender).  .......
    else
      if Sender is voilier then
       voilier(Sender). .......
   ............
end;

Remonter



instanciation dans un type ascendant (Dangereux et déconseillé)

Il s'agit ici d'une utilisation non licite qui n'est pas commune à tous les langages cités plus haut. Les compilateurs C# et Java refuseront cet type de création d'objet, C++ et Delphi l'accepterons en laissant au programmeur le soin de se débrouiller avec les problèmes de cohérence lorsqu'ils apparaîtront.

En Delphi il est tout à fait possible, de créer un objet de classe parent référencé par une variable de classe enfant en transtypant la reférence de départ. Reprenons l'exemple de la hiérarchie des véhicules utilisé plus haut :
 

 
var
 x : vehicule;
 y : voiture;
 z : break;
...
/ / instanciation dans le type initial :
x := vehicule.Create;

// objet de classe parent vehicule référencé par y de classe enfant voiture transtypé :
vehicule ( y ) := vehicule.Create;

// objet de classe parent terrestre référencé par z de classe enfant break transtypé :
terrestre ( z ) := terrestre.Create;

// objet de classe parent voiture référencé par même z de classe enfant break transtypé :
voiture ( z ) := voiture.Create;

etc...

Dangereux car le fait par exemple de créer pour z : break;  un objet voiture ( z ) := voiture.Create; permet l'utilisation syntaxique de champs inexistant dans la classe parent ! Par exemple si la classe break comporte un champ hayon que la classe voiture n'avait pas, le compilateur ne se rendra pas compte que z.hayon est une erreur lorsqu'après l'écriture voiture ( z ) := voiture.Create , la reférence z pointe vers un objet voiture (un break a un hayon , un voiture en général n'a pas de hayon sauf si c'est un break...)