5.3 Polymorphisme avec Delphi


  Plan de ce chapitre:  

1.Polymorphisme d'objet

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


2.Polymorphisme de méthode

Introduction
Vocabulaire et concepts
Surcharge dans la même classe
Surcharge dans une classe dérivée
Surcharge dynamique dans une classe dérivée
Répartition des méthodes en Delphi
Réutilisation de méthodes avec inherited


3.Polymorphisme de classe abstraite

Introduction
Vocabulaire et concepts

 

4.Exercice traité sur le polymorphisme

 



1.Polymorphisme d'objet

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

 

Il existe un concept essentiel en POO désignant la capacité d'une hiérarchie de classes à fournir différentes implémentations de méthodes portant le même nom et par corollaire la capacité qu'ont des objets enfants de modifier les comportements hérités de leur parents. Ce concept d'adaptation à différentes "situations" se dénomme le polymorphisme qui peut être implémenté de différentes manières.

 

Polymorphisme d'objet - définition générale

C'est une interchangeabilité entre variables d'objets de classes de la même hiérarchie sous certaines conditions,  que dénommons le polymorphisme d'objet.

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 quel 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érarchie, c'est cette variabilité que dénommons le polymorphisme d'objet.

Nous allons dès lors envisager toutes les situations possibles et les évaluer, les exemples appuyant le texte sont présentés en Delphi.


 

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.


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

 

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. il existe 3 possibilités différentes, la figure ci-dessous indique les affectations possibles :

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

Les trois possibilités sont :

  • L'instanciation et l'utilisation de références dans le même type
  • L'affectation de références :  polymorphisme implicite
  • L'affectation de références :   polymorphisme par transtypage d'objet

La dernière de ces possibilités pose un problème d'exécution lorsqu'elle est mal employée !

 

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).

 

                                     

Nous pouvons en effet dire que x peut se référer implicitement à tout objet de classe Mere ou de toute classe héritant 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 la figure fig-2 ci-dessus 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 pratiques figurées ci-dessous : (Mere=vehicule, Fille1=terrestre, Fille2=voiture, Fille3=marin, Fille4=voilier, Fille5=croiseur)

Une hiérarchie de classes de véhicules descendant toutes de la classe mère Vehicule.

Déclaration de cette hiérarchie en

Vehicule = class
  ..........
end; 

 
terrestre = class (Vehicule)  

 ..........
end; 


voiture = class (terrestre)
  ..........
end;  


Marin = class (Vehicule)  
 ..........
 end;

 voilier = class (marin)
  ..........
 
end;

 
croiseur = class (marin)
 ..........
 end;

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 traduisant cette phrase 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.
Polymorphisme implicite =  création d'objet de classe descendante référencé par une variable parent

 

Quand peut-on écrire x := y sur des objets ?

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.

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 du  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 :

 

 

 

 

La nouvelle hiérarchie est la suivante :

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

  • on crée une voiture référencée par la variable x de classe vehicule,
  • on crée une berline  référencée par la variable y  de classe voiture,
  • enfin on crée un break référencé par la variable z de classe break.

 

Réécrivons ces phrases afin de comprendre à quel genre de situation pratique cette opération correspond :

on crée un véhicule du type voiture

on crée une voiture de type berline

enfin on crée un break de type  break

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

 

Polymorphisme d'objet explicite par transtypage

Reprenons le code précédent en extrayant la partie qui nous intéresse :

var
  x , ObjM : Mere;
  y : 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 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 ).

La situation informatique est la suivante :

  • on déclare une variable x de type Mere,
  • on déclare une variable y de type Fille héritant de Mere,
  • on instancie la variable x dans le type descendant Fille (polymorphisme implicite).

Il est alors possible de faire "pointer" la variable y (de type Fille) vers l'objet (de type Fille) auquel se réfère x en effectuant une affectation de références.

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

var
  x : Mere;
  y  : Fille;

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

Dans la dernière instruction, la reférence ObjM est transtypée en type Fille, 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 voiture acceptera de pointer vers cet objet, en particulier la variable y :voiture après transtypage de la référence de x.

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

En Delphi l'affectation s'écrit 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 disposent d'un opérateur permettant de tester cette appartenance ou plutôt l'appartenance à une hiérarchie.

 
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.

 


L'opérateur  "as" en

 

L'opérateur as est un opérateur de transtypage de référence d'objet semblable à  l'opérateur ( ). 
L'opérateur as fournit la valeur null en cas d'échec de conversion alors que l'opérateur ( ) lève une exception.
 
(objet   as   classeT)  renvoie une référence de type classeT
 
Exemple d'utilisation des deux opérateurs :
 
var x : classeT;
if objet   is   classeT then
  x := objet   as   classeT ;

 



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 est 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 définies plus haut :

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

  
2.
Polymorphisme de méthode


Introduction

Lorsqu'une classe enfant hérite d'une classe mère, des méthodes supplémentaires nouvelles peuvent être implémentées dans la classe enfant mais aussi des méthodes des parents redéfinies pour obtenir des implémentations différentes.

Une classe dérivée hérite de tous les membres de sa classe parent ; c'est-à-dire que tous les membres du parent sont disponibles pour l'enfant, rappelons qu'une méthode est un membre qualifiant un comportement d'un objet de la classe. En POO on distingue deux catégories de méthodes selon les besoins des applications et du polymorphisme : les méthodes statiques et les méthodes dynamiques.
 

2.1 Vocabulaire et concepts généraux :

 

  • L'action qui consiste à donner le même nom à plusieurs méthodes dans la même classe ou d'une classe parent à une classe enfant, se dénomme d'une manière générale la surcharge de nom de méthode (avec ou non la même signature).

 

  • Le vocabulaire n'étant pas stabilisé selon les auteurs (surcharge, redéfinition, substitution,...) nous employerons les mots redéfinition, surcharge dynamique ou substitution dans le même sens, en précisant lorsque cela s'avérera nécessaire de quel genre de laison il s'agit.

 

 

Les actions des méthodes héritées du parent peuvent être modifiés par l'enfant de deux manières, selon le type de liaison du code utilisé pour la méthode (la liaison statique ou précoce ou bien la liaison dynamique ou retardée). 

 

   Les deux modes de laison du code d'une méthode
 

La liaison statique ou précoce (early-binding) :

  •   Lorsqu'une méthode à liaison statique est invoquée dans le corps d'un programme, le compilateur établit immédiatement dans le code appelant l'adresse précise et connue du code de la méthode à invoquer. Lors de l'exécution c'est donc toujours le même code invoqué.


La liaison dynamique ou retardée (lazy-binding) :

  • Lorsqu'une méthode à liaison dynamique est invoquée dans le corps d'un programme, le compilateur n'établit pas immédiatement dans le code appelant l'adresse de la méthode à invoquer. Le compilateur met en place un mécanisme de reférence (référence vide lors de la compilation) qui, lors de l'exécution, désignera (pointera vers) le code que l'on voudra invoquer; on pourra donc invoquer des codes différents.

 


2.2 Surcharge dans la même classe :

Dans une classe donnée, plusieurs méthodes peuvent avoir le même nom, mais les signatures des méthodes ainsi surchargées doivent obligatoirement être différentes et peuvent éventuellement avoir des niveaux de visibilité différents.


Nous avons déjà vu les bases de ce type de surcharge lors de l'étude de Delphi et la POO. Soit par exemple, la classe ClasseA ci-dessous, ayant 3 méthodes de même nom P, elles sont surchargées dans la classe selon 3 signatures différentes :

Classe A
  public methode P(x,y);
  privé methode P(a,b,c);
  protégé methode P( );
finClasse A

La première surcharge de P dispose de 2 paramètres, la seconde de 3 paramètres, la dernière enfin n'a pas de paramètres. C'est le compilateur du langage qui devra faire le choix pour sélectionner le code de la bonne méthode à utiliser. Pour indiquer ce genre de surcharge, en Delphi il faut utiliser un qualificateur particulier dénoté overload.

Syntaxe de l'exemple en Delphi, en Java et en C# :

Delphi

Java - C#

ClasseA = class
  public
      procedure P(x,y : integer);overload;
  private
      procedure P(a,b,c : string); overload;
  protected
      procedure P;overload;
end;

class ClasseA  {
  public void P( int x,y ){ }
  private void P( String a,b,c ){ }
  protected void P( ){ }
}


Utilisation pratique : permettre à une méthode d'accepter plusieurs types de paramètres en conservant le même nom, comme dans le cas d'opérateur arithmétique travaillant sur les entiers, les réels,...

Exemple de code Delphi :

ClasseA = class
  public
      procedure P(x,y : integer);overload;
      procedure P(a,b,c : string);overload;
      procedure P;overload;
end; var Obj:ClasseA;
.....
Obj := ClasseA.create;
Obj.P( 10, 5 );
Obj.P( 'abc', 'ef', 'ghi' );
Obj.P; 

2.3 Surcharge statique dans une classe dérivée :

D'une manière générale, Delphi et C# disposent par défaut de la notion de méthode statique, Java n'en dispose pas sauf dans le cas des méthodes de classes. Dans l'exemple ci-dessous en Delphi et en C#, les trois méthodes P,Q et R sont à liaison statique dans leur déclaration par défaut sans utiliser de qualificateur spécial.
 

Delphi

C#

ClasseA = class
  public
      procedure P(x,y : integer);
  private
      procedure Q(a,b,c : string);
  protected
      procedure R;
end;
class ClasseA  {
  public void P( int x,y ){ }
  private void Q( String a,b,c ){ }
  protected void R( ){ }
}

 
Une classe dérivée peut masquer une méthode à liaison statique héritée en définissant une nouvelle méthode avec le même nom.

Si vous déclarez dans une classe dérivée, une méthode ayant le même nom qu'une méthode à liaison statique d'une classe ancêtre, la nouvelle méthode remplace simplement la méthode héritée dans la classe dérivée.


Dans ce cas nous employerons aussi le mot de masquage qui semble être utilisé par beaucoup d'auteurs pour dénommer ce remplacement, car il correspond bien à l'idée d'un masquage "local" dans la classe fille du code de la méthode de la classe parent par le code de la méthode fille.

Ci-dessous un exemple de hiérarchie de classes et de masquages successifs licites de méthodes à liaison statiques dans certaines classes dérivées avec ou sans modification de visibilité :
 

   Classe A

  public statique methode P;
  privé statique methode Q;
  protégé statique methode R;

   finClasse A

   Classe B hérite de Classe A

  public statique methode P;
  privé statique methode Q;
  protégé statique methode R;

   finClasse B

   Classe C hérite de Classe B

  protégé statique methode P;
  privé statique methode Q;
 

   finClasse C

   Classe D hérite de Classe C

  public statique methode P;
 

   finClasse D

   Classe E hérite de Classe D

  protégé statique methode P;
   

   finClasse E

   Classe F hérite de Classe E

  privé statique methode P;
  public statique methode R;

   finClasse F

Dans le code d'implémentation de la Classe F :

La méthode P utilisée est celle qui définie dans la Classe F et elle masque la méthode P de la Classe E.
La methode Q
utilisée est celle qui définie dans la Classe C.
La méthode R utilisée est celle qui définie dans la Classe F et elle masque la méthode R de la Classe B

 

Soit en Delphi l'écriture des classes ClasseA et ClasseB de la hiérarchie ci-haut :

Delphi

Explications

ClasseA = class
  public
      procedure P(x,y : integer);
  private
      procedure Q(a,b,c : string);
  protected
      procedure R;
end; 


ClasseB = class ( ClasseA )
  public
      procedure P(u : char);
  private
      procedure Q(a,b,c : string);
  protected
      procedure R(x,y : real);
end;


Dans la classe ClasseB :

La méthode procedure P(u : char) surcharge statiquement (masque) avec une autre signature, la méthode héritée de sa classe parent procedure P(x,y : integer).

La méthode procedure Q(a,b,c : string) surcharge statiquement (masque) avec la même signature, la méthode héritée de sa classe parent procedure Q(a,b,c : string).

La méthode procedure R(x,y : real) surcharge statiquement (masque) avec une autre signature, la méthode héritée de sa classe parent procedure R.

Utilisation pratique : Possibilité notamment de définir un nouveau comportement lié à la classe descendante et éventuellement de changer le niveau de visibilité de la méthode.

Exemple de code Delphi :


ClasseA = class
  public
      procedure P(x,y : integer);
      procedure Q(a,b,c : string);
      procedure R;
end;

 
ClasseB = class ( ClasseA )
  public
      procedure P(u : char);
      procedure Q(a,b,c : string);
      procedure R(x,y : real);
end;
........
.........
var ObjA:ClasseA;
       ObjB:ClasseB;
.....
ObjA := ClasseA.create;
ObjA.P( 10, 5 );
ObjA.Q( 'abc', 'ef', 'ghi' );
ObjA.R;
.........
ObjB := ClasseB.create;
ObjB.P( 'g' );
ObjB.Q( 'abc', 'ef', 'ghi' );
ObjB.R( 1.2, -5.36 );
 

 

2.4 Surcharge dynamique dans une classe dérivée :

Un type dérivé peut redéfinir (surcharger dynamiquement) une méthode à liaison dynamique héritée. On  appelle aussi virtuelle une telle méthode à liaison dynamique, nous utiliserons donc souvent ce raccourci de notation pour désigner une méthode surchargeable dynamiquement.
 
L'action de redéfinition fournit une nouvelle définition de la méthode qui sera appelée en fonction du type de l'objet au moment de l'exécution et non du type de la variable de reférence connue au moment de la compilation

Ci-dessous un exemple de hiérarchie de classes et de redéfinitions (surcharges dynamiques) successives fictives de méthodes à liaison dynamique dans certaines classes dérivées, pour les modifications de visibilité il faut étudier le manuel de chaque langage :

   Classe A
  public dynamique methode P;
  privé dynamique methode Q;
  protégé dynamique methode R;
   finClasse A
   Classe B hérite de Classe A
  public dynamique methode P;
  privé dynamique methode Q;
  protégé dynamique methode R;
   finClasse B
   Classe C hérite de Classe B
  protégé dynamique methode P;
  privé dynamique methode Q;
 
   finClasse C
   Classe D hérite de Classe C
  public dynamique methode P;
 

   finClasse D
   Classe E hérite de Classe D
  protégé dynamique methode P;
 

   finClasse E
   Classe F hérite de Classe E
  privé dynamique methode P;
  public dynamique methode R;

   finClasse F

Remarque pratique :

 Une méthode redéfinissant une méthode virtuelle peut selon les langages changer le niveau de visibilité ( il est conseillé de laisser la nouvelle méthode redéfinie au moins aussi visible que la méthode virtuelle parent).

                                                   Tableau comparatif liaison dynamique-statique

Liaison statique

Liaison dynamique

Lors d'un appel pendant l'exécution leur liaison est très rapide car le compilateur a généré l'adresse précise du code de la méthode lors de la compilation.

Lors d'un appel pendant l'exécution leur liaison plus lente car l'adresse précise du code de la méthode est obtenu par un processus de recherche dans une structure de données.

Une telle méthode fonctionne comme une procédure ou fonction d'un langage non orienté objet et ne permet pas le polymorphisme. Car lors d'un appel pendant l'exécution c'est toujours le même code qui est exécuté quel que soit le type de l'objet qui l'invoque.

Une telle méthode autorise le polymorphisme, car bien que portant le même nom dans une hiérarchie de classe, lors d'un appel pendant l'exécution c'est toujours le type de l'objet qui l'invoque qui déclenche le mécanisme de recherche du code adéquat.

 

 

2.5 La répartition des méthodes en Delphi
 

Le terme de répartition des méthodes est synonyme de liaison et fait référence à la façon dont un programme détermine où il doit rechercher le code d'une méthode lorsqu'il rencontre un appel à cette méthode.

 

En Delphi, il existe trois modes de répartition des méthodes qui peuvent être  :

  •   Statiques ,
  •   Virtuelles,
  •   Dynamiques.


Méthodes statiques en Delphi


Les méthodes statiques de Delphi sont des méthodes à liaison précoce.



Voyons ce qui se passe lorsque dans la classe fille on tente de "redéfinir" une méthode héritée de la classe mère. Ci-après nous tentons de "redéfinir" la méthode Deux :


Lors de l'exécution de l'appel Obj.Deux, rien n'a changé. En effet lors de la compilation la variable Obj est déclarée de type MaClasse, c'est donc l'adresse du code de la méthode Deux de la classe MaClasse qui est liée. Le type SousClasse de l'objet réel vers lequel pointe Obj pendant l'exécution n'a aucune influence sur le mode de répartition :

  • Cela signifie qu'il est impossible de redéfinir une méthode statique P; c'est toujours le même code qui est exécuté, quelqu soit la classe dans laquelle P est appelée.


  • Si l'on déclare dans une classe dérivée une méthode portant le même nom qu'une méthode statique de la classe mère avec la même signature ou bien avec une signature différente, la nouvelle méthode remplace simplement la méthode héritée dans la classe dérivée, nous dirons qu'elle masque la méthode mère.




Méthodes virtuelles en Delphi

Les méthodes virtuelles utilisent un mécanisme de répartition nécessitant une recherche contrairement aux méthodes statiques. Une méthode virtuelle peut être redéfinie dans les classes descendantes sans masquer ses différentes versions dans les classes ancêtres.

 

A l'opposé d'une méthode statique l'adresse du code de la méthode virtuelle n'est pas déterminée lors de la compilation, mais seulement lors de l'exécution et en fonction du type de l'objet qui l'appelle.

Les méthodes virtuelles de Delphi sont des méthodes à liaison tardive. 

Pour déclarer une méthode virtuelle, il faut ajouter le qualificateur virtual à la fin de la déclaration de l'en-tête de la méthode :

procedure P(x,y : integer); virtual ;

Comment se passe la liaison dynamique avec Delphi ? 

Delphi implante d'une façon classique le mécanisme de liaison dynamique :


  • Lors de la compilation, Delphi rajoute à chaque classe une Table des Méthodes Virtuelles (TMV). Cette table contient en principal, pour chaque méthode déclarée avec le qualificateur virtual :
  • un pointeur sur le code de la méthode,
  • la taille de l'objet lui-même

  • Donc chaque objet possède sa propre TMV , et elle est unique.

 

  • La TMV d'un objet est créée avec des pointeurs vides lors de la compilation, elle est remplie lors de l'exécution du programme ( plus précisément lors de l'instanciation de l'objet) car c'est à l'exécution que l'adresse du code de la méthode est connue et donc stockée dans la TMV.

 

  • En fait c'est le constructeur de l'objet lors de son instanciation qui lance le stockage dans la TMV des adresses de toutes les méthodes virtuelles de l'objet, à la fois les méthodes héritées et les méthodes nouvelles.

 
Lorsque l'on construit une nouvelle classe héritant d'une autre classe mère, la nouvelle classe récupère dans sa TMV toutes les entrées de la TMV de sa classe mère, plus les nouvelles entrées correspondant aux méthodes virtuelles déclarées dans la nouvelle classe. Une TMV est donc une structure de données qui grossit au cours de l'héritage de classe et peut être assez volumineuse pour des objets de classes situées en fin de hiérarchie.
 

Redéfinition de méthode virtuelle avec Delphi

Pour redéfinir une méthode virtuelle dans les classes descendantes sans masquer ses différentes versions dans les classes ancêtres, il faut ajouter à la fin de sa déclaration d'en-tête le qualificateur override.

 
Reprenons l'exemple précédent et tentons de "redéfinir" la méthode Deux cette fois en la déclarant virtuelle dans la classe mère et redéfinie dans la classe fille, puis comparons le comportement polymorphique de la méthode Deux selon qu'elle est virtuelle ou statique  :


Méthodes dynamiques en Delphi


Les méthodes dynamiques sont des méthodes virtuelles avec un mécanisme de répartition différent, donc les méthodes dynamiques de Delphi sont des méthodes à liaison tardive.

  • Au lieu d'être stockées dans la Table des Méthodes Virtuelles, les méthodes dynamiques sont ajoutées dans une structure de données de liste spécifique pour chaque objet (la liste des méthodes dynamiques). 

  • Seules les adresses des méthodes nouvelles ou redéfinies d'une classe sont rangées dans sa liste.

La liaison précode d'une méthode dynamique héritée s'effectue en recherchant dans la liste des méthodes dynamiques de chaque ancêtre, en remontant la hiérarchie de l'héritage.


Pour déclarer une méthode dynamique, il faut ajouter le qualificateur dynamic à la fin de la déclaration de l'en-tête de la méthode :

procedure P(x,y : integer); dynamic ;

 

 Toute méthode sans qualification particulière est considérée comme statique par défaut.


  • Comme les méthodes dynamiques ne disposent pas d'entrées dans la table des méthodes virtuelles de l'objet, elles réduisent la quantité de mémoire occupée par les objets.

  • Si une méthode est appelée fréquemment, ou si le temps d'exécution est un paramètre important, il vaut mieux déclarer une méthode virtuelle plutôt que dynamique.

  

Masquage de méthode virtuelle avec Delphi

Pour masquer une méthode virtuelle dans une de ses classes, il suffit de ne pas ajouter à la fin de sa déclaration d'en-tête le qualificateur override


Ceci peut se faire de deux façons soit en masquant par une méthode statique, soit en masquant par une méthode dynamique.

 

Masquage par une méthode dynamique

Dans le code suivant, la méthode Deux déclarée virtuelle dans la classe mère, est masquée par une méthode Deux ayant la même signature et déclarée elle aussi virtuelle dans la classe fille :

 

type

 MaClasse=class
    Etat:string;
    procedure Un;     //statique 
    procedure Deux; virtual;     //virtuelle
 end;

  SousClasse=class(MaClasse)
    procedure Un ;     //statique, masque la méthode ancêtre 
    procedure Deux;virtual;    // virtuelle, masque la méthode ancêtre, mais elle-même est redéfinissable
 end;


comparons le comportement polymorphique de la méthode Deux selon qu'elle est redéfinie, masquée par une méthode statique ou masquée par une méthode virtuelle  :



Nous pouvons conclure de ce tableau de comparaison que le masquage (par une méthode statique ou virtuelle) ne permet jamais le polymorphisme.


Le polymorphisme de méthode offre la possibilité de conserver le même nom de méthode dans une hiérarchie de classe, afin de ne pas "surcharger" le cerveau de l'utilisateur. Les comportements seront différents selon le type d'objet utilisé.

Par exemple l'action de Démarrer dans une hiérarchie de véhicules :


Nous voyons bien que sémantiquement parlant on peut dire qu'une voiture démarre, qu'un voilier démarre, qu'un croiseur démarre, toutefois les actions internes permettant le comportement démarrage ne sont pas les mêmes.

 

  • Démarrer dans la classe voiture : tourner la clef de contact, engager une vitesse,...

 

  • Démarrer dans la classe voilier : hisser les voiles, dégager la barre,...

 

  • Démarrer dans la classe croiseur : lancer les moteurs, modifier la barre,...

L'action Démarrer est polymorphe (car elle s'adapte au type de véhicule qui l'exécute).

Pour traduire en Delphi, ce comportement polymorphe de l'action Démarrer, nous allons utiliser une méthode virtuelle que nous redéfinissons dans toutes les classes dérivées.

Soit en Delphi l'écriture de l'exemple de la hiérarchie précédente :

Delphi

Vehicule = class
  public
      procedure Demarrer; virtual; //virtuelle
end;

Terrestre = class ( Vehicule )
  .....
end;

Voiture = class ( Terrestre )
  public
      procedure Demarrer; override; //redéfinie
end;

Marin = class ( Vehicule )
  .....
end;

Voilier = class ( Marin )
  public
      procedure Demarrer; override; //redéfinie
end;

Croiseur = class ( Marin )
  public
      procedure Demarrer; override; //redéfinie
end;

 Exemple de code d'utilisation :


Vehicule = class
  public
      procedure Demarrer; virtual; (1)
end;

 
Voiture = class ( Terrestre )
  public
      procedure Demarrer; override; (2)
end;
........
var Vehic1 : Vehicule;
       Auto1, Auto2 : Voiture;
 
.....
Vehic1 := Vehicule.Create; //instanciation dans le type
Auto1 := Voiture.Create; //instanciation dans le type
Auto2 := Voiture.Create; //instanciation dans le type
Vehic1.Demarrer; //méthode du type Vehicule (1)
Auto1.Demarrer; //méthode du type Voiture (2)
Auto2.Demarrer; //méthode du type Voiture (2)
Vehic1 := Auto1; //pointe vers un objet de type hérité
Vehic1.Demarrer; // polymorphisme à l'œuvre ici: (2)
.........
Vehic1 := Voiture.Create; //instanciation un type hérité
Vehic1.Demarrer; // polymorphisme à l'œuvre ici: (2)
 

Illustrons l'exemple précédent avec une image de la partie de code généré sur la méthode Demarrer et ensuite l'exécution de ce code sur des objets effectifs. Nous avons numéroté (1) et (2) les deux codes d'implantation de la méthode Demarrer dans la classe parent et dans la classe enfant :


Le code engendré contient des reférences vides

 

Lors de l'exécution après mécanisme de répartition selon le genre de liaison et le type de l'objet, les reférences pointent vers le code de la méthode du type effectif de l'objet.


2.6 Réutilisation de méthodes avec
inherited


Le mot réservé inherited joue dans Delphi un rôle particulier dans l'implémentation de comportements polymorphiques (il joue un rôle semblable à super dans java et base dans C#).


Il ne peut qu'apparaître dans une définition de méthode avec ou sans identificateur à la suite.

  • Si inherited présent dans une methode P de la classeA est suivi par le nom d'une méthode Q, il représente un appel normal de la méthode Q, sauf que la recherche de la méthode à invoquer commence dans l'ancêtre immédiat de la classe de la méthode et remonte la hiérarchie.

  • Si inherited présent dans une methode P de la classeA, n'est suivi d'aucun nom de méthode, il représente alors un appel à une méthode de même nom P, la recherche de la méthode à invoquer commence dans l'ancêtre immédiat de la classe de la méthode P actuelle et remonte la hiérarchie.

 
 

3.Polymorphisme de classes abstraites

Introduction

Les classes abstraites permettent de créer des classes génériques expliquant certains comportements sans les implémenter et fournissant une implémentation commune de certains autres comportements pour l'héritage de classes. Les classes abstraites sont un outil précieux pour le polymorphisme.
 

Vocabulaire et concepts :

  • Une classe abstraite est une classe qui ne peut pas être instanciée.
  • Une classe abstraite peut contenir des méthodes déjà implémentées.
  • Une classe abstraite peut contenir des méthodes non implémentées.
  • Une classe abstraite est héritable.
  • On peut contsruire une hiérarchie de classes abstraites.
  • Pour pouvoir construire un objet à partir d'une classe abstraite, il faut dériver une classe non abstraite en une classe implémentant toutes les méthodes non implémentées.
  •  Une méthode déclarée dans une classe, non implémentée dans cette classe,  mais juste définie par la déclaration de sa signature, est dénommée méthode abstraite.

  • Une méthode abstraite est une méthode à liaison dynamique n’ayant pas d’implémentation dans la classe où elle est déclarée. L' implémentation d'une méthode abstraite est déléguée à une classe dérivée.

Si vous voulez utiliser la notion de classe abstraite pour fournir un comportement polymorphe à un groupe de classes, elles doivent toutes hériter de la même classe abstaite, comme dans l'exemple ci-dessous :


La classe Véhicule est abstraite, car la méthode Démarrer est abstraite et sert de "modèle" aux futures classes dérivant de Véhicule, c'est dans les classes voiture, voilier et croiseur que l'on implémente le comportement précis du genre de démarrage.

Notons au passage que dans la hiérarchie précédente, les classes vehicule Terrestre et Marin héritent de la classe Véhicule, mais n'implémentent pas la méthode abstraite Démarrer, ce sont donc par construction des classes abstraites elles aussi.

 Les classes abstraites peuvent également contenir des membres déjà implémentés. Dans cette éventualité, une classe abstraite propose un certain nombre de fonctionnalités identiques pour tous ses futurs descendants (ceci n'est pas possible avec une interface).

Exemple : la classe abstraite Véhicule  n'implémente pas la méthode abstraite Démarrer, mais fournit et implante une méthode "RépartirPassagers" de répartition des passagers à bord du véhicule (fonction de la forme, du nombre de places, du personnel chargé de s'occuper de faire fonctionner le véhicule...), elle fournit aussi et implante une méthode "PériodicitéMaintenance" renvoyant la périodicité de la maintenance obligatoire du véhicule (fonction du nombre de km ou miles parcourus, du nombre d'heures d'activités,...)

Ce qui signifie que toutes les classes voiture, voilier et croiseur savent comment  répartir leurs éventuels passagers et quand effectuer une maintenance, chacune d'elle implémente son propre comportement de démarrage.

 Syntaxe de l'exemple en Delphi et en Java  :

Delphi

Java 

Vehicule = class
  public
      procedure Demarrer; virtual;abstract;
      procedure RépartirPassagers; virtual;
      procedure PériodicitéMaintenance; virtual;
end;

   abstract class ClasseA  {

  public abstract void Demarrer( );
  public void RépartirPassagers( );
  public void PériodicitéMaintenance( );
}

 
Utilisez une classe abstraite lorsque vous voulez :

  • regrouper un ensemble de méthodes présentant des fonctionnalités identiques,
  • déléguer l'implémentation de certaines méthodes à une classe dérivée,
  • disposer immédiatement de fonctionnalités concrètes pour d'autres méthodes,
  • assurer un comportement polymorphe aux méthodes dont on délègue l'implantation du code à des classes dérivées.

 Exemple de code Delphi pour la hiérarchie ci-dessous :


Soit en Delphi l'écriture d'un exemple tiré de cette hiérarchie :

Delphi

  Unit UclassVehicules;

interface

Vehicule = class
  public
      procedure Demarrer; virtual;abstract;
      procedure RépartirPassagers; virtual;
      procedure PériodicitéMaintenance; virtual;
end;

Terrestre = class ( Vehicule )
  public
      procedure PériodicitéMaintenance; override;
end;

Voiture = class ( Terrestre )
  public
      procedure Demarrer; override;
      procedure RépartirPassagers; override;
end;

Marin = class ( Vehicule )
  public
      procedure PériodicitéMaintenance; override;
end;

Voilier = class ( Marin )
  public
      procedure Demarrer; override;
      procedure RépartirPassagers; override;
end;

Croiseur = class ( Marin )
  public
      procedure Demarrer; override;
      procedure RépartirPassagers; override;
end;

implementation

//--- les méthodes implantées de la classe abstraite Vehicule :
 procedure Vehicule.RépartirPassagers; 
 begin
   ..........
 end;

 procedure Vehicule.PériodicitéMaintenance;
 begin
   ..........
 end;

//--- les méthodes implantées de la classe abstraite Terrestre :
 procedure Terrestre.PériodicitéMaintenance; 
 begin
   ..........
 end;

//--- les méthodes implantées de la classe abstraite Marin :
 procedure Marin.PériodicitéMaintenance; 
 begin
   ..........
 end;

//--- les méthodes implantées de la classe Voiture :
 procedure Voiture.Demarrer; 
 begin
   ..........
 end;
 procedure Voiture.RépartirPassagers; 
 begin
   ..........
 end;
//--- les méthodes implantées de la classe Voilier :
 procedure Voilier.Demarrer; 
 begin
   ..........
 end;
 procedure Voilier.RépartirPassagers; 
 begin
   ..........
 end;
//--- les méthodes implantées de la classe Croiseur :
 procedure Croiseur.Demarrer; 
 begin
   ..........
 end;
 procedure Croiseur.RépartirPassagers; 
 begin
   ..........
 end;

  end.

Dans cet exemple :

Les classes Vehicule, Marin et Terrestre sont abstraites car aucune n'implémente la méthode abstraite Demarrer

Les classes Marin et Terrestre contiennent chacune une surcharge dynamique implémentée de la méthode virtuelle PériodicitéMaintenance qui est déjà implémentée dans la classe Véhicule.

Les classes Voiture, Voilier et Croiseur ne sont pas abstraites car elles implémentent les (la) méthodes abstraites de leurs parents.