La programmation à objets. Application au langage Eiffel
2e partie

Dans le précédent numéro de Linux Magazine, nous avons passé en revue quelques-uns des concepts de base de la programmation à objets (les classes etl'héritage). Nous allons aujourd'hui en étudier d'autres, peut-être plus complexes, mais aussi plus puissants, dont l'héritage multiple et la généricité.

SmallEiffel, le compilateur GNU Eiffel, en est actuellement à sa 23e version, la version -0.78 (comme vous pouvez le remarquer, la numérotation des versions est inversée, la première version était numérotée -0.99), et est disponible sur de nombreuses plates-formes (Amiga, BSD, GNU/Linux, NEXT, Solaris, Windows 95& NT...). C'est un compilateur Eiffel qui génère, comme code intermédiaire, soit du C, soit du bytecode Java. Vous pouvez vous le procurer à http://SmallEiffel.loria.fr afin d'essayer les exemples que nous vous proposons.

L'héritage multiple

Le mécanisme d'héritage multiple proposé par C++ est trop rudimentaire et complexe en ce qui concerne le renommage, la redéfinition et la visibilité des méthodes héritées pour être utilisable (d'ailleurs, de nombreuses règles de programmation C++ recommandent de ne pas utiliser l'héritage multiple). Eiffel, quant à lui, maîtrise le puissant potentiel de l'héritage multiple par un mécanisme plus clair et plus complet.

On peut également remarquer qu'en Eiffel, on peut hériter de primitives (attributs et routines complètes, pas seulement de prototypes) venant de plusieurs classes alors qu'en Java, on récupère du code d'un endroit (héritage) et des prototypes complètement vides de plusieurs endroits (interfaces)...

L'héritage multiple peut être utile à plusieurs titres. Tout d'abord, pour permettre une modélisation plus concrète, plus proche du monde réel. Ainsi, par exemple, on peut avoir les classes suivantes :

class HOTEL

feature

make_reservation: INTEGER is

require

available_rooms > 0

do

... -- le code qui fait cela

ensure

available_rooms = (old available_rooms) - 1

end

available_rooms: INTEGER is

do

... -- le code qui fait cela

end

feature {NONE}

rooms: ARRAY[ROOM]

...

end

class RESTAURANT

feature

make_reservation: INTEGER is

require

available_tables > 0

do

... -- le code qui fait cela

ensure

available_tables = (old available_tables) - 1

end

available_tables: INTEGER is

do

...

end

feature {NONE}

tables: ARRAY[TABLE]

...

end

Notons en passant la présence du mot clé old, autorisé dans les postconditions, qui signifie « la valeur qu'avait l'expression qui suit le old au moment de l'appel de la routine ». Ceci permet de modéliser des changements temporels simples et de mieux contrôler les actions d'une routine.

Fort logiquement, un hôtel-restaurant est, tant du point de vue conceptuel (typage) que du point de vue de la réutilisation, un héritier d'HOTEL et de RESTAURANT. Cet héritage multiple s'exprime tout simplement en Eiffel :

class HOTEL_RESTAURANT

inherit

HOTEL

rename make_reservation as make_room_reservation

end

RESTAURANT

rename make_reservation as make_table_reservation

end

end

Ainsi, l'HOTEL_RESTAURANT réutilise non seulement l'interface (profils des routines et leurs assertions) mais également le code des ses deux ancêtres, sans aucune modification. De simples renommages ont été effectués pour éviter une collision sur le nom 'make_reservation' et différencier la réservation d'une table de la réservation d'une chambre.

Il est bien entendu également possible, si nécessaire, de redéfinir le corps d'une routine :

class MA_CLASSE

inherit MON_PARENT

redefine ma_routine_a_modifier

end

feature

ma_routine_a_modifier is

do

... -- nouveau corps

end

end

La Généricité

Jusqu'à présent, une classe définissait un type unique. La généricité permet de définir une famille de types (souvent des conteneurs : piles, listes, tableaux...) ou, plus formellement, des types paramétrés. La généricité est partiellement disponible en C++ via les templates, mais absente en Java et Smalltalk. Examinons un extrait de la classe générique ARRAY, qui fait partie de la bibliothèque de base du langage et qui implante les tableaux d'objets de toutes sortes.

class ARRAY[G]

item(i: INTEGER): G is

do

...

end

Cette classe est paramétrée par une variable de type générique G. Dans le contexte de la classe ARRAY, le type formel des éléments du tableau est G. Ainsi, la méthode 'item', qui accède à l'élément i du tableau, renvoie un objet de type G. Lors de la déclaration d'une variable (locale ou attribut) de type tableau, le paramètre formel G est remplacé par un paramètre correspondant à un type réel, concret (par exemple ARRAY[INTEGER], ARRAY[ARRAY[STRING]]...). On obtient ainsi de nouveaux types, sans avoir à écrire de nouvelle classe. Le code de la classe ARRAY écrit, débogué et optimisé une seule fois devient (ré)utilisable dans de multiples contextes pour un coût de développement constant. Une classe peut bien entendu être paramétrée par plusieurs types, comme nous allons le voir sur un exemple particulier de la généricité : la généricité contrainte.

La généricité contrainte est un raffinement de la généricité. Examinons la classe DICTIONARY, qui stocke des associations entre une clé et une valeur :

class DICTIONARY[V,K->HASHABLE]

--

-- Associative memory.

-- Values of type `V' are stored using Keys of type `K'.

--

Dans cette définition, on retrouve les 2 éléments génériques de l'association : le type de la clé 'K' et le type de la valeur stockée 'V'. Le type de la clé est contraint (opérateur '->') par le type HASHABLE (une classe héritant de HASHABLE doit définir une méthode 'hash_code' permettant de calculer une clé de hachage). Cela signifie que, lors de la déclaration d'un dictionnaire, la classe correspondant à la clé devra être une descendante de la classe HASHABLE. L'intérêt de cette généricité contrainte est que l'on sait, de cette façon, que la clé, dont le type K n'est pas connu, dispose (au moins) des primitives accessibles sur des objets de type HASHABLE. Dans ce cas précis, HASHABLE ne fournit qu'une seule primitive, celle qui retourne le code de hachage associé à l'objet :

deferred class HASHABLE

feature

hash_code: INTEGER is

-- The hash-code value of `Current'.

deferred

ensure

good_hash_value: Result >= 0

end;

end -- HASHABLE

Grâce à la généricité contrainte, on sait dans le code de DICTIONARY que K est compatible avec HASHABLE et donc que l'on peut écrire :

put(v: V; k: K) is

-- If there is as yet no key `k' in the dictiona-

ry, enter

-- it with item `v'. Otherwise overwrite the item associated

-- with key `k'.

local

hash: INTEGER;

do

...

hash := k.hash_code \ modulus;

...

end;

Ainsi, l'association entre des clients d'un magasin et des informations relatives à ces clients pourrait par exemple être simplement stockée dans un objet de type

DICTIONNARY[CLIENT, STRING] (la classe STRING, qui implante les chaînes de caractères, fait partie de la bibliothèque standard et hérite de la classe HASHABLE).

Les assertions

Les assertions sont du code de vérification, c'est-à-dire des expressions booléennes qui doivent toujours être évaluées à vrai, faute de quoi une exception est déclenchée.

Eiffel permet la programmation par contrat. Le client d'une méthode (l'appelant) doit respecter un contrat (des préconditions introduites dans une clause 'require'), en échange de quoi le fournisseur (la méthode) garantit l'exécution du contrat (une postcondition, introduite par une clause 'ensure'). Ces deux clauses constituent des assertions. Elles ont plusieurs fonctions qui font d'elles des outils très puissants. Tout d'abord, elles permettent d'utiliser Eiffel comme un outil de spécification : en phase de conception, il est possible de décrire les propriétés et les relations entre classes avant d'avoir écrit la moindre ligne de code "actif" dans le corps des méthodes. Ensuite, elles permettent de documenter de façon élégante et constructive le code écrit. Enfin, et surtout, elles ont pour fonction de tester le code écrit et fournissent ainsi une aide capitale pour le débogage, tant en phase de développement qu'en phase de maintenance. Des bibliothèques de composants bien construites sont dotées d'assertions qui, non seulement vérifient l'implantation de la bibliothèque, mais également, via les préconditions, que le code client qui les utilise est bien conçu.

L'utilisation d'assertions se traduit naturellement par la génération de code de vérification supplémentaire. Plusieurs niveaux de compilation sont ainsi disponibles, qui activent plus ou moins d'assertions, en fonction de la maturité du logiciel développé : -debug_check, -all_check, -ensure_check, -require_check, -no_check et enfin -boost avec laquelle le logiciel est supposé correct et donc, plus aucun code relatif aux assertions n'est alors généré. L'utilisation des assertions peut être illustrée par la méthode 'insert' de la classe STRING de la bibliothèque standard.

insert(ch: CHARACTER, index: INTEGER) is

-- Insert 'ch' after position 'index'

require

0 <= index and index <= count

local

i : INTEGER

do

from

i:= count

extend(' ')

until

i = index

loop

put(item(i), i+1);

i := i + 1

end

put(ch, index+1 );

ensure

item( index+1 ) = ch

end

La précondition spécifie que la méthode 'insert' ne peut s'exécuter que si les deux conditions '0 <= index' et 'index <= count' sont vérifiées (c'est-à-dire si l'appelant essaie bien d'insérer un caractère à une position valide de la chaîne). Si cette précondition est vérifiée, la méthode s'engage alors à respecter sa postcondition, à savoir positionner le caractère 'ch' après la position 'index'. La fonction documentaire des assertions est plus facilement perceptible avec l'interface de cette routine obtenue avec la commande 'short' :

insert (ch: CHARACTER; index: INTEGER)

-- Insert ch after position index.

require

0 <= index and index <= count

ensure

item(index + 1) = ch

Il existe en Eiffel d'autres sortes d'assertions, parmi lesquelles l'invariant de classe. Cet invariant définit un ensemble de propriétés qui doivent être vérifiées à chaque utilisation d'un objet de la classe. Par exemple, dans la classe STRING, on trouve les déclarations suivantes :

class STRING

feature {NONE}

storage: NATIVE_ARRAY[CHARACTER];

-- The place where characters are stored.

feature

count: INTEGER;

-- String length.

capacity: INTEGER;

-- Capacity of the `storage'.

invariant

0 <= count;

count <= capacity;

capacity > 0 implies storage.is_not_null;

end -- STRING

La puissance d'expression des assertions est encore augmentée par la possibilité offerte par le langage d'hériter des assertions des classes parentes. Ainsi, le contrôle d'intégrité apporté par les assertions est réutilisé et enrichi de la même façon que le code lui-même. Une fois encore, le développement est grandement facilité, puisqu'une classe bénéficie du travail de documentation et de test déjà effectué pour ses ancêtres. Ceci constitue une différence capitale avec les mécanismes d'assertion beaucoup plus primitifs tels que l'assert de C/C++, purement syntaxique.

La Mémoire

La mémoire est une abstraction qui ne doit (devrait ?) pas être manipulée directement dans un langage objet digne de ce nom. Ainsi, tous les programmes générés par un compilateur Eiffel disposent d'un ramasse-miettes (garbage collector en anglais ou, plus simplement, GC) qui a en charge la récupération automatique de la mémoire allouée par le programme. Libéré de cette charge qui est source de nombreuses bogues en C ou C++, le développeur peut se concentrer sur d'autres aspects de la conception. En Java, le principe est le même, à ceci près que le ramasse-miettes est intégré à la machine virtuelle qui exécute le bytecode (les fichiers *.class). Le lecteur attentif aura noté que le langage Scheme, présenté dans de précédents numéros de Linux Mag', comme la majorité des langages fonctionnels, dispose lui aussi d'un GC. Il semble d'ailleurs que tous les langages modernes intègrent maintenant en standard un ramasse-miettes à l'exception de cas atypiques.

Des mécanismes sont prévus avec SmallEiffel pour faire face aux cas particuliers et permettre de générer, lors de la compilation, un programme sans ramasse-miettes (option -no_gc) ou d'activer et de désactiver le ramasse-miettes par programme à certains moments de l'exécution (voir la classe MEMORY pour en savoir plus).

Philippe Coucaud, Olivier Zendra, Dominique Colnet

SmallEiffel Home Page : http://SmallEiffel.loria.fr

Newsgroup : comp.lang.eiffel


© 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".