La programmation à objets

Application au langage Eiffel - 3e partie

Nous allons, dans cette troisième partie de notre introduction à la programmation à objets, détailler deux de ses concepts les plus fondamentaux : le polymorphisme et la liaison dynamique. Nous allons ensuite finir de présenter la syntaxe Eiffel avec les structures de contrôle évoluées.

Rappelons que dans les deux précédents numéros de Linux Magazine, nous avions expliqué les concepts de base de la programmation à objets (classe, objet, héritage simple et multiple, généricité) ainsi que des notions de programmation par contrat (à l'aide d'assertions) et de gestion mémoire.

Nous continuerons à illustrer notre propos à l'aide d'exemples écrits en Eiffel, langage objet puissant et pourtant facile. Vous pourrez essayer ces exemples à l'aide de SmallEiffel, le compilateur GNU Eiffel, développé au laboratoire LORIA de Nancy. Il est accessible gratuitement à http://SmallEiffel.loria.fr, avec l'ensemble de son code source.

Type statique, type dynamique

Dans un langage de programmation «classique», comme C ou Pascal, une variable (ou, de façon plus générale, une expression quelconque) a un type et un seul, statique, qui ne variera pas au cours de l'exécution. Ce type est connu lors de la compilation, il s'agit de celui qui jouxte le nom de la variable là où elle est déclarée. Ainsi, par exemple, dans les déclarations C suivantes

int mon_entier;

struct client mon_client;

mon_entier est de type entier (int) alors que mon_client est une structure de type client. Ceci reste immuable et permet au compilateur de générer le code approprié au type de la variable considérée ainsi que d'effectuer des vérifications de type pour détecter les erreurs commises par le programmeur. En raison de contraintes de place, nous ne considérerons pas ici les «casts» C, qui permettent de forcer le type de la source d'une affectation et - bien que parfois nécessaires - occasionnent de nombreuses erreurs.

Dans un langage à objets, ce type, celui de la déclaration, qui apparaît dans le code source à côté du nom de la variable, est appelé «type statique». Il joue globalement le même rôle que pour un langage classique.

En revanche, dans les langages à objets, une variable (ou toute expression) possède également un «type dynamique». Comme son nom l'indique, il peut changer dynamiquement, lors de l'exécution. Le type dynamique d'une variable, à un instant précis de l'exécution, est en fait le type de l'objet attaché à cette variable à cet instant.

Polymorphisme

Le «polymorphisme» est justement le fait qu'une variable puisse avoir plusieurs types dynamiques à l'exécution (du grec «poly» plusieurs et «morphë», forme).

Comment cela est-il possible et à quoi cela sert-il ? En deux mots, et de manière un peu abstraite, ceci permet d'accéder aux primitives d'un (objet d'un) certain type de la même façon qu'à celles d'un (objet d'un) de ses sous-types. Ceci permet de substituer à un type l'un de ses sous-types, sans que ce changement n'apparaisse au niveau des clients.

Pour donner une explication plus concrète, nous allons reprendre un des exemples que nous avions utilisés dans le premier article de cette série :

deferred class POINT

feature

x: REAL is deferred end

y: REAL is deferred end

alpha: REAL is deferred end

d: REAL is deferred end

is_origin: BOOLEAN is

do

Result := (x = 0.0) and (y = 0.0)

end;

end;

class POINT_CARTESIEN

inherit POINT

creation

make

feature

make (abscisse, ordonnee: REAL) is

do

x := abscisse;

y := ordonne;

end

x: REAL

y: REAL

alpha: REAL is

do

Result := (y/x).atan.to_real

end

d: REAL is

do

Result := (x^2 + y^2).sqrt

end

end

class POINT_POLAIRE

inherit POINT

creation make

feature

make (angle, distance: REAL) is

do

alpha := angle;

d := distance;

end

x: REAL is

do

Result := d.cos(alpha)

end

y: REAL is

do

Result := d.sin(alpha)

end

alpha: REAL

d: REAL

end

Supposons que nous ayons une routine cliente comme suit :

do_something (p:POINT) is

do

if p.is_origin then

-- faire quelque chose

else

-- faire autre chose

end

end

La routine do_something prend un POINT en paramètre et lui applique la fonction is_origin. Ici, le type statique de p est POINT, mais son type dynamique peut être aussi bien POINT que l'un de ses descendants (POINT_CARTESIEN ou POINT_POLAIRE).

Ainsi, p est une variable polymorphe. Dans notre exemple, comme POINT est une classe abstraite (mot clé deferred), le lecteur attentif se rappellera qu'elle ne peut être instanciée, comme nous l'avons indiqué dans notre premier article. Il ne sera donc possible d'avoir que des objets de type POINT_CARTESIEN ou POINT_POLAIRE attachés à la variable polymorphique p. Comme is_origin existe au niveau de POINT, elle existe aussi dans ses descendants. Il n'est donc pas nécessaire de se soucier du type dynamique de p, car on peut substituer à POINT n'importe lequel de ses descendants et la routine is_origin adéquate, qui sera appelée. p.is_origin, est donc un appel statiquement valide.

Liaison dynamique

Le fait de sélectionner à l'exécution la routine adéquate s'appelle justement la «liaison dynamique».

Dans l'exemple ci-dessus, les trois types de points partagent tous la même routine is_origin (celle définie dans POINT). Il n'est donc pas nécessaire de faire appel à la liaison dynamique : l'appel à la bonne routine peut être codé correctement lors de la compilation. Supposons en revanche que l'on ait le client suivant :

do_something (p:POINT) is

do

if p.d < 1.0 then

-- faire quelque chose

else

-- faire autre chose

end

end

L'appel à p.d est statiquement valide puisque POINT, qui est le type statique de p, comprend bien une routine d. Cependant, celle-ci est codée différemment selon que l'on a affaire à un POINT_CARTESIEN ou à un POINT_POLAIRE. Il est donc nécessaire, à l'exécution, «d'aiguiller» l'appel p.d sur la routine correspondant au type dynamique de p. Si p est un POINT_POLAIRE, il s'agira d'un simple accès à un champ de l'objet p ; si p est un POINT_CARTESIEN, il s'agira d'un appel de fonction.

Du point de vue de l'implantation, il est donc nécessaire, afin de pouvoir effectuer l'opération de liaison dynamique, de disposer à tout moment du type des objets. Ceux-ci se voient donc attacher à leur création une information supplémentaire qui est leur identifiant de type. Cet identifiant peut consister en un entier codant le numéro du type ou un pointeur vers la description du type. Il implique un léger surcoût en termes de taille et de temps d'exécution, qui peut d'ailleurs être éliminé partiellement ou en totalité par des compilateurs Eiffel dotés d'importantes capacités d'optimisation de code, comme SmallEiffel.

En revanche, dans un langage classique, non objet, aucune trace explicite de ce type n'existe plus dans la représentation des données à l'exécution. Le code généré l'a été en connaissant le type des données qu'il manipule, mais rien ne permet plus, au simple vu des données, de déterminer à quel type elles correspondent.

On voit donc que les langages à objets offrent une bien plus grande flexibilité, une meilleure facilité d'utilisation et un pouvoir expressif bien supérieur aux langages classiques, au prix d'une légère baisse des performances (qui, rappelons-le, peut être gommée par de bons compilateurs). Il s'agit là d'une tendance générale en informatique, puisque l'on évolue sans cesse de langages de bas niveau vers des langages plus puissants, un peu plus gourmands en ressources machines mais permettant de faire des économies importantes en temps de développement.

Structures de contrôle en Eiffel :

Maintenant que nous avons vu les principaux concepts des langages à objets en général, et d'Eiffel en particulier, nous allons pouvoir nous consacrer à ce dernier plus précisément, en complétant les quelques bases déjà données de sa syntaxe. La spécification de l'ensemble de cette syntaxe est accessible (en anglais) depuis l'adresse suivante :

http://www.gobosoft.com/eiffel/syntax/index.html

Eiffel est un langage qui allie puissance, élégance et compacité du langage. Ce dernier point signifie que sa syntaxe est très lisible et facilement compréhensible pour le débutant. Un des moyens pour y arriver a été, par exemple, de n'avoir qu'une seule structure de contrôle pour tous les types d'itérations.

Une boucle «de base» en Eiffel se présente sous la forme suivante :

from

bloc_d_initialisation

until

condition_d'arret

loop

corps_de_la_boucle

end

Le bloc d'initialisation comprend les instructions d'initialisation de la boucle (en général, initialisation d'un indice, par exemple). Il peut être vide. Notons qu'il n'y a aucune différence à l'exécution entre une instruction écrite juste avant le from et une écrite dans le bloc d'initialisation ; il s'agit simplement d'une séparation destinée à faciliter la lecture.

La condition d'arrêt est une expression booléenne et est obligatoire.

Enfin, le corps contient les instructions à exécuter dans l'itération. C'est ici également que prennent place les instructions qui vont faire varier «l'indice» de boucle ou ce qui en tient lieu.

Avec cette structure, tous les types d'itération peuvent être exprimés. Ainsi, l'équivalent de l'instruction C :

for (i=0; i<n; i++) {...}

s'exprime en Eiffel :

from

i:=0

until

i>=n

loop

...

i := i + 1;

end

Notez que la condition de continuation de la structure C a été transformée (par simple négation) en la condition d'arrêt équivalente en Eiffel. Le while condition {...} et le

do {...} while condition du langage C se traduisent également aisément.

Contrairement à la plupart des autres langages, Eiffel offre la possibilité de «sécuriser» ses itérations à l'aide de deux types d'assertions : le variant de boucle et l'invariant de boucle. Voici la syntaxe qui s'y rapporte :

from

bloc_d_initialisation

invariant

assertion

variant

expression_entiere

until

condition_d_arret

loop

corps_de_la_boucle

end

L'invariant et le variant ne sont pas obligatoires. L'invariant sert à indiquer une condition qui doit être vraie tout au long de la boucle (par exemple, que l'indice de boucle ne doit pas déborder des bornes d'un tableau).

Le variant, lui, est une expression entière, dont la valeur doit décroître à chaque étape de l'itération, tout en restant supérieure à zéro. Il sert à garantir que l'itération progresse, mais qu'elle se finira un jour (le variant à zéro déclenchant une violation d'assertion avec un message d'erreur explicite). La terminaison de l'itération est ainsi assurée, évitant les boucles sans fin. Bien entendu, ces assertions de boucle, comme toutes les assertions Eiffel, peuvent être activées ou non lors de la compilation, selon que l'on est en train de tester son programme ou que l'on produise la version finale, optimisée, dont on est (à peu près) sûr qu'elle soit correcte.

Nous avons également déjà vu des instructions conditionnelles simples, comme le if...then...else, qu'il n'est pas besoin d'expliquer. Notons simplement qu'il existe une petite variante syntaxique bien pratique, qui permet d'éviter d'imbriquer des suites de conditionnelles :

if condition1 then

bloc_alors1

elseif condition2 then

bloc_alors2

elseif conditions3 then

bloc_alors3

...

else

bloc_sinon

end

Il existe bien entendu aussi en Eiffel une instruction de choix multiple (analogue au «case» ou au «switch» de nombreux langages). Il s'agit du inspect :

inspect

expression

when valeur1 then

bloc1

when valeur2 then

bloc2

when valeur3 then

bloc3

else

bloc_sinon

end

L'expression qui sert «d'aiguillage» doit être de type INTEGER, CHARACTER ou BOOLEAN. Selon la valeur qu'elle prend à l'exécution, un seul des choix sera valide. Les différents cas possibles (les différents when) doivent être disjoints et leur ordre n'a aucune importance (contrairement au if...then...else). Ils sont exprimés sous forme de valeur(s) ou d'intervalles de valeurs. Les blocs sont des suites d'instructions (éventuellement vides). Voici un exemple d'inspect complet sur une expression de type INTEGER :

inspect

mon_entier

when 0 then

bloc_zero

when 1,2,3 then

bloc_1_a_3

when 4..9 then

bloc_4_a_9

when 11, 13..17, 19 then

bloc_plus_complique

else

les_negatifs_10_18_et_plus_de_20

end

Il faut noter que si ce genre d'aiguillage est très souvent utilisé dans des langages comme le C, par exemple pour discriminer différents cas en testant le type des données, ceci est beaucoup moins vrai dans les langages à objets. En effet, ces derniers disposent du mécanisme de liaison dynamique qui, d'une certaine manière, joue ce rôle en appelant des routines adaptées au type dynamique des objets. Dans notre pratique d'Eiffel, nous avons constaté que les inspect sont assez souvent utilisés dans le cadre des entrées sorties et assez peu pour la «logique interne» des applications.

A suivre...

Nous avons maintenant fini de détailler les concepts objets et les mécanismes syntaxiques et sémantiques d'Eiffel. Nous aborderons dans le prochain article les bibliothèques Eiffel et certaines classes de base.

Olivier Zendra,

Dominique Colnet,

Philippe Coucaud


© Copyright 2000 Diamond Editions/Linux magazine France. - Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1or any later version published by the Free Software Foundation; A copy of the license is included in the section entitled "GNU Free Documentation License".