Guide Pascal et Delphi
Date de publication : 10/04/2000 , Date de mise à jour : 01/04/2008
XVI. Programmation à l'aide d'objets
XVI-A. Introduction
XVI-B. Concepts généraux
XVI-B-1. De la programmation traditionnelle à la programmation objet
XVI-B-2. La programmation (orientée ?) objet
XVI-B-3. Classes
XVI-B-4. Objets
XVI-B-5. Fonctionnement par envoi de messages
XVI-B-6. Constructeur et Destructeur
XVI-C. Bases de la programmation objet sous Delphi
XVI-C-1. Préliminaire : les différentes versions de Delphi
XVI-C-2. Définition de classes
XVI-C-3. Déclaration et utilisation d'objets
XVI-C-4. Utilisation d'un constructeur et d'un destructeur, notions sur l'héritage
XVI-C-5. Visibilité des membres d'une classe
XVI-C-6. Propriétés
XVI-C-6-a. Propriétés simples
XVI-C-6-b. Propriétés tableaux
XVI-C-6-c. Propriété tableau par défaut
XVI-C-7. Mini-projet n°5 : Tableaux associatifs
XVI-D. Notions avancées de programmation objet
XVI-D-1. Retour sur l'héritage
XVI-D-2. Polymorphisme
XVI-D-3. Surcharge et redéfinition des méthodes d'une classe
XVI-D-4. Méthodes abstraites
XVI-D-5. Méthodes de classe
XVI-E. Conclusion
XVI. Programmation à l'aide d'objets
XVI-A. Introduction
Ce chapitre est un prolongement du
chapitre 12 sur
l'utilisation des objets. Ce chapitre vous donnait le rôle d'utilisateur des objets,
en vous donnant quelques explications de base sur les mécanismes de l'utilisation
des objets. Ce chapitre a pour ambition de vous donner toutes les clés
indispensables pour bien comprendre et utiliser les objets tels qu'ils sont
utilisables dans le langage de Delphi : Pascal Objet.
Certaines notions du chapitre 10 vont être ici reprises afin de les approfondir ou
de les voir sous un jour différent. D'autres, complètement nouvelles pour vous, vont
être introduites au fur et à mesure de votre progression dans cet immense chapitre.
Afin de ne pas vous perdre dés le début, je vous propose un court descriptif de ce
qui va suivre.
Dans un premier temps, nous allons aborder les concepts de base des objets. Ainsi,
les termes de "programmation (orientée) objets", de classe, d'objet, de méthode, de
champ vont être définis. Par la suite, nous aborderons les sections privées,
protégées, publiques, publiées des classes, la notion de hiérarchie de classe, ainsi
que des notions connexes comme l'héritage, le polymorphisme et l'encapsulation. Nous
aborderons ensuite des notions plus spécifiques à Delphi : les propriétés, les
composants et leur création. Enfin, nous aborderons les deux notions incontournables
d'exception et d'interface.
Ne vous effrayez pas trop devant l'ampleur de la tâche, et bonne lecture !
XVI-B. Concepts généraux
Cette partie a pour but de vous présenter les notions essentielles à la
compréhension du sujet traité dans ce chapitre. Nous partirons des notions de base
que vous connaissez pour aborder les concepts fondamentaux de la programmation
objet. Nous aborderons ainsi les notions de classe, d'objet, de champ et de méthode.
Cette partie reste très théorique : si vous connaissez déjà la programmation objet,
vous pouvez passer à la suivante, sinon, lisez attentivement ce qui suit avant
d'attaquer la partie du chapitre plus spécifiquement dédiée à Delphi.
XVI-B-1. De la programmation traditionnelle à la programmation objet
Le style de programmation que l'on appelle communément « programmation objet » est
apparue récemment dans l'histoire de la programmation. C'est un style à la fois
très particulier et très commode pour de nombreuses situations. Jusqu'à son
invention, régnait, entre autres, la programmation impérative, c'est-à-dire un
style faisant grande utilisation de procédures, de fonctions, de variables, de
types de données plus ou moins évolués, et de pointeurs. Les langages C ou Pascal,
qui figurent parmi les plus connus des non-initiés en sont de très bons
exemples.
Le style de programmation impératif, très pratique pour de petits programme système
ou de petites applications non graphiques existe toujours et a encore un avenir
radieux devant lui, mais en ce qui concerne les grosses applications graphiques,
ses limites ont rapidement été détectées, ce qui a forcé le développement d'un
nouveau style de programmation, plus adapté au développement d'applications
graphiques. L'objectif était de pouvoir réutiliser des groupes d'instructions pour
créer de nouveaux éléments, et ce sans avoir à réécrire les instructions en
question. Si cela ne vous paraît pas forcément très clair, abordons un petit
exemple.
Lorsque vous utilisez une application graphique, vous utilisez une interface faite
de fenêtres, de boutons, de menus, de zones d'édition, de cases à cocher. Chacun de
ces éléments doit pouvoir être affiché et masqué à volonté pendant l'exécution
d'une application les utilisant. En programmation impérative, il faudrait écrire
autant de procédures d'affichage et de masquage que de types de composants
utilisables, à savoir des dizaines. Leur code source présenterait d'évidentes
similarités, ce qui serait assez irritant puisqu'il faudrait sans cesse réécrire
les mêmes choses, d'où une perte de temps non négligeable. L'un des concepts de la
programmation objets est de permettre d'écrire une seule fois une procédure
d'affichage et une procédure de masquage, et de pouvoir les réutiliser à volonté
dans de nouveaux éléments sans avoir à les réécrire (nous verrons comment plus
tard).
Bien que délicate à aborder et à présenter au départ, la programmation objet
réserve de très bonnes surprises, à vous de les découvrir dans les paragraphes qui
suivent.
XVI-B-2. La programmation (orientée ?) objet
Pascal Objet est un langage dit orienté objet. Que signifie cette dénomination
?
Il existe en fait deux catégories de langages qui permettent la programmation
utilisant les objets :
- Les langages ne permettant rien d'autre que l'utilisation d'objets, dans
lesquels tout est objet. Ces langages sont assurément appréciés par certaines personnes
mais imposent un apprentissage des plus délicats.
- Les langages permettant l'utilisation des objets en même temps que le style
impératif classique. C'est le cas de Pascal Objet, et de nombreux autres langages comme
Java et C++. Ces langages sont beaucoup plus souples même si nombre d'entre eux ne
donnent pas les mêmes possibilités en termes de programmation objet.
Delphi supporte donc un langage orienté objet, ce qui signifie que quelqu'un qui
ignore tout des objets peut presque entièrement s'en affranchir, ou les utiliser
sans vraiment s'en rendre compte (ce qui a été votre cas jusqu'au chapitre 9 de ce
guide). Limitée dans la version 1 de Delphi, la « couche » (l'ensemble des
fonctionnalités) objet de la version 6 est réellement digne de ce nom. Mais trêve
de blabla, passons aux choses sérieuses.
XVI-B-3. Classes
Le premier terme à comprendre lorsqu'on s'attaque à la programmation objet est
incontestablement celui de "classe", qui regroupe une bonne partie de la
philosophie objet. Ce paragraphe va aborder en douceur cette notion souvent mal
comprise des débutants pour tenter de vous éviter ce genre de souci.
Une classe est le pendant informatique d'une notion, d'un concept. Si dans la vie
courante vous vous dite : ceci est un concept, une notion bien précise possédant
plusieurs aspects ou facettes, alors la chose en question, si vous devez la
représenter en informatique, le sera sous forme de classe.
La classe est l'élément informatique qui vous permettra de transcrire au mieux un
concept de la vie concrète dans un langage informatique. Une classe permet de
définir les données relatives à une notion, ainsi que les actions qu'y s'y
rapportent. Prenons tout de suite un exemple : celui de la télévision. Le concept
général de télévision se rattache, comme toute notion, à des données (voire
d'autres concepts) ainsi que des actions. Les données sont par exemple le poids, la
longueur de la diagonale de l'écran, les chaînes mémorisées ainsi que l'état
allumé/veille/éteint de la télévision. Les actions peuvent consister à changer de
chaîne, à allumer, éteindre ou mettre en veille la télévision. D'autres actions
peuvent consister en un réglage des chaînes. Le concept de télévision peut être
raccordé à un concept appelé "magnétoscope", qui aura sa propre liste de données et
d'actions propres. Voici une représentation possible du concept de télévision :

En informatique, la représentation idéale pour une télévision sera donc une classe.
Il sera possible, au sein d'une seule et même structure informatique, de regrouper
le pendant informatique des données – des variables – et le pendant informatique
des actions, à savoir des procédures et des fonctions. Les variables à l'intérieur
d'une classe seront appelées champs, et les procédures et les fonctions
seront appelées méthodes. Les champs et les méthodes devront possèder des
noms utilisables en temps normal pour des variables ou des procédures/fonctions.
Voici un schémas représentant non plus le concept de télévision, mais la
classe « Television » ; notez les changements de couleurs, de noms et les
mots importants tels que "classe", "champs" et "méthodes" :

Tout comme un concept n'est pas une réalisation pratique, nous verrons qu'une
classe ne l'est pas au niveau informatique : c'est une sorte, un moule. Le concept
de télévision, qui ne vous permet pas d'en regarder une chez vous le soir, ni ne
vous permet d'en régler les chaînes ne vous suffit pas à lui seul : il va vous
falloir sa réalisation pratique, à savoir un exemplaire physique d'une télévision.
Ceci sera également valable pour les classes, mais nous en reparlerons bientôt : ce
seront les fameux objets.
Parlons maintenant des écrans d'ordinateur : ils sont également caractérisés par
leur poids, leur diagonale, mais là où la télévision s'intéresse aux chaînes,
l'écran d'ordinateur possède plutôt une liste de résolutions d'écran possibles,
ainsi qu'une résolution actuelle. Un tel écran peut en outre être allumé, éteint ou
en veille et doit pouvoir passer d'un de ces modes aux autres par des actions. En
informatique, le concept d'écran d'ordinateur pourra être représenté par une classe
définissant par exemple un réel destiné à enregistrer le poids, un entier destiné à
mémoriser la diagonale en centimètres. Une méthode (donc une action transcrite au
niveau informatique) permettra de l'allumer et une autre de l'éteindre, par
exemple.
Si l'on s'en tient à ce qui vient d'être dit, et en adoptant la même représentation
simple et classique que précédemment pour les concepts, nous voici avec ceci :

Comme vous pouvez le constater, les deux diagrammes ci-dessus présentent des
similarités flagrantes, comme la longueur de la diagonale, le poids et le fait
d'éteindre ou d'allumer l'appareil. Ne serait-il pas intéressant de choisir une
approche différente mettant en avant les caractéristiques communes, et de
particulariser ensuite ?
Explications : Une télévision et un écran d'ordinateur sont tous deux des appareils
à écran. Un appareil à écran possède un poids, une longueur de diagonale et il est
possible de l'éteindre et de l'allumer. Une télévision est un tel appareil,
possédant également une liste de chaînes mémorisées, une action permettant de
mémoriser une chaîne, ainsi que d'autres non précisées ici qui font d'un appareil à
écran... une télévision. Un écran d'ordinateur est également un appareil à écran,
mais possédant une résolution actuelle, une liste de résolutions possibles, et une
action permettant de changer la résolution actuelle. La télévision d'un coté, et
l'écran d'ordinateur de l'autre sont deux concepts basés sur celui de l'appareil à
écran, et lui ajoutant divers éléments qui le rendent particulier. Si je vous dis
que j'ai un appareil à écran devant moi, vous pouvez légitimement penser qu'il peut
s'agire d'une télévision, d'un écran d'ordinateur, voire d'autre chose...
Cette manière de voir les choses est l'un des fondements de la programmation objet
qui permet (et se base) sur ce genre de regroupements permettant une réutilisation
d'éléments déjà programmés dans un cadre général. Voici le diagramme précédent,
modifié pour faire apparaître ce que l'on obtient par regroupement des informations
et actions redondantes des deux concepts (un nouveau concept intermédiaire, appelé
"appareil à écran" a été introduit à cette fin) :

L'intérêt de ce genre de représentation est qu'on a regroupé dans un seul concept
plus général ce qui était commun à deux concepts proches l'un de l'autre. On a
ainsi un concept de base très général, et deux concepts un peu plus particuliers
s'appuyant sur ce concept plus général mais partant chacun d'un coté en le
spécialisant. Lorsqu'il s'agira d'écrire du code source à partir des concepts, il
ne faudra écrire les éléments communs qu'une seule fois au lieu de deux (ou plus),
ce qui facilitera leur mise à jour : Il n'y aura plus de risque de modifier par
exemple le code source réalisant l'allumage d'une télévision sans modifier
également celui d'un écran d'ordinateur puisque les deux codes source seront en
fait un seul et même morceau de code général applicable à n'importe quel appareil à
écran.
Au niveau de la programmation objet, nous avons déjà vu qu'un concept peut être
représenté par une classe et avons déjà observé la représentation d'une classe. Le
fait qu'un concept s'appuie sur un autre plus général et y ajoute des données et
des actions fera de ce concept une seconde classe, dont on dira qu'elle
étend la première classe. Il est maintenant temps de s'attaquer à la
traduction des concepts en classes. Voici la représentation du diagramme précédent
en terme de classes, sans encore aborder le code source ; notez que les
regroupements sont identiques à ce qui est fait au niveau conceptuel, et donc que
les deux diagrammes sont très proches l'un de l'autre :

La programmation objet accorde une grande importance à ce genre de schémas :
lorsqu'il sera question d'écrire un logiciel utilisant les capacités de la
programmation objet, il faudra d'abord modéliser les concepts à manipuler
sous forme de diagramme de concepts, puis traduire les diagrammes de concepts en
diagrammes de classes. Lorsque vous aurez plus d'expérience, vous pourrez
directement produire les diagrammes de classe, seuls utilisables en
informatique.
Résumons ce qu'il est nécessaire d'avoir retenu de ce paragraphe :
- Une classe est la représentation informatique d'un concept
- Un concept possède des données et des actions.
- Une classe possède la représentation informatique des données –
des champs – et la représentation informatique des actions : des méthodes.
- Tout comme un concept peut s'appuyer sur un autre concept et le
particulariser, une classe peut étendre une autre classe en ajoutant
des champs (données) et des méthodes (actions).
Voici un tableau vous montrant les 3 facettes d'un même élément dans les 3 mondes
que vous connaissez désormais : le vôtre, celui de la programmation objet, et celui
de la programmation impérative (sans objets). Vous pouvez y voir la plus forte
lacune de la programmation impérative, à savoir qu'un concept ne peut pas y être
modélisé directement.
| Monde réel |
Programmation objet |
Programmation impérative |
| Concept |
Classe |
(rien)
|
| Donnée |
Champ |
Variable |
| Action |
Méthode |
Procédure Fonction |
XVI-B-4. Objets
Nous avons vu précédemment que les classes étaient le pendant informatique des
concepts. Les objets sont pour leur part le pendant informatique de la
représentation réelle des concepts, à savoir des occurrences bien réelles de ce qui
ne serait sans cela que des notions sans application possible. Une classe n'étant
que la modélisation informatique d'un concept, elle ne se suffit pas à elle-même et
ne permet pas de manipuler les occurrences de ce concept. Pour cela, on utilise les
objets.
Voici un petit exemple : le concept de télévision est quelque chose que vous
connaissez, mais comment ? Au travers de ses occurrences physiques, bien entendu.
Il ne vous suffit pas de savoir qu'une télévision est un appareil électronique à
écran, ayant un poids et une largeur de diagonale, qu'on peut éteindre, allumer, et
dont on peut régler les chaînes : le soir en arrivant chez vous, vous allumez UNE
télévision, c'est-à-dire un modèle précis, possédant un poids défini, une taille de
diagonale précise. Si vous réglez les chaînes, vous ne les réglez que pour VOTRE
télévision et non pour le concept de télévision lui-même !
En informatique, ce sera la même chose. Pour manipuler une occurrence du concept
modélisé par une classe, vous utiliserez un objet. Un objet est dit d'une
classe particulière, ou instance d'une classe donnée. En programmation
objet, un objet ressemble à une variable et on pourrait dire en abusant un petit
peu que son type serait une classe définie auparavant. Si cela peut vous aider,
vous pouvez garder en tête pour l'instant que les objets sont des formes évoluées
de variables, et que les classes sont des formes évoluées de types ; je dis "pour
l'instant" car il faudra petit à petit abandonner cette analogie pleine de
pièges.
Un objet est en réalité un élément variable possédant un exemplaire personnel de
chaque champ défini par sa classe. Ainsi, si on crée deux objets "Tele1" et "Tele2"
de classe "Television", chacun aura un champ "poids" et un champ
"longueur_diagonale". Il est impossible de lire ou d'écrire la valeur d'un champ
d'une classe, mais il devient possible de lire ou d'écrire la valeur d'un champ
d'un objet. Le fait de modifier la valeur d'un champ pour un objet ne modifie pas
la valeur du même champ d'un autre objet de même classe. Ainsi, le fait de dire que
la valeur du champ "poids" de l'objet Tele1 est 23 ne modifie pas la valeur du
champ "poids" de l'objet "Tele2", qui peut valoir par exemple 16.
Les méthodes sont partagées par tous les objets d'une même classe, mais une méthode
peut être appliquée non pas à la classe qui la définit, mais à un objet de cette
classe. Les champs manipulés par la méthode sont ceux de l'objet qui a reçu l'appel
de méthode. Ainsi, le fait d'appeler la méthode "eteindre" de l'objet "Tele2" fixe
la valeur du champ "allume" de cet objet ("Tele2") à faux, mais n'a pas accès à
l'objet "Tele1" à laquelle elle ne s'applique pas. Pour éteindre "Tele1", il faudra
appeler la méthode "eteindre" de cet objet ("Tele1"). Ceci permet d'avoir pour
chaque objet une copie des champs avec des valeurs personnalisées, et des méthodes
s'appliquant à ces champs uniquement. C'est pour cela qu'on introduit souvent la
programmation objet en présentant le besoin de regrouper données et instructions
dans une même structure, à savoir l'objet.
Il est possible de manipuler un nombre quelconque d'objets d'une classe
particulière (c'est-à-dire que vous pouvez par exemple avoir un nombre quelconque
de télévisions, mais le concept de télévision reste unique), mais un objet est
toujours d'une seule et unique classe (vous ne pouvez pas faire d'une télévision un
casse-noisettes, encore que, comme dans la vie réelle, ce genre de chose soit en
fait possible (comprenez toléré) mais d'un intérêt fort discutable !). En fait,
nous verrons qu'un objet peut être restreint à une classe. Par exemple, une
télévision pourra être temporairement considérée comme un appareil à écran, mais
cela ne change en rien sa nature de télévision.
XVI-B-5. Fonctionnement par envoi de messages
Maintenant que vous connaissez les objets, il vous reste à comprendre comment ils
vont fonctionner ensembles. En effet, un objet possède des champs et des méthodes,
mais reste quelque chose d'inanimé. Pour faire fonctionner un objet, il faut lui
envoyer un message. L'envoi d'un message consiste tout simplement à effectuer un
appel de méthode sur cet objet. L'objet qui reçoit un tel message exécute la
méthode correspondante sur ses champs et retourne éventuellement un résultat. Le
fait de modifier directement la valeur d'un champ d'un objet (sans appeler de
méthode) ne constitue pas un envoi de message.
Il est intéressant de mettre en lien la notion de message à celle d'événement car
ces deux notions sont dépendantes l'une de l'autre. Lorsque dans votre application
un événement se produit, par exemple lorsqu'on clique sur un bouton, un message est
envoyé à la fiche qui contient ce bouton. La fiche est en fait un objet d'une
classe particulière, et le traitement du message consiste à exécuter une méthode de
l'objet représentant la fiche, et donc à répondre à l'événement par des actions.
Vous voyez au passage que vous utilisez depuis longtemps les envois de messages,
les objets et les classes sans toutefois les connaître sous ce nom ni d'une manière
profonde.
Dans les langages purement objet, seuls les envois de messages permettent de faire
avancer l'exécution d'une application. Par exemple, pour effectuer 1+2, vous
envoyez le message "addition" à l'objet 1 avec le paramètre 2, le résultat est le
nouevl objet 3. Pascal Objet est heureusement pour vous un langage orienté
objet, et à ce titre vous pouvez utiliser autre chose que des objets et donc ne pas
trop vous attarder sur les messages.
Si je vous ai présenté les messages, c'est à titre d'information uniquement. Je
suis persuadé que nombre de programmeurs n'ont jamais entendu parlé de l'envoi de
messages en programmation objet, ce qui fait que vous pouvez vous en tenir à la
notion d'appel de méthodes en sachant toutefois que cela correspond à l'envoi d'un
message.
XVI-B-6. Constructeur et Destructeur
Les objets étant des structures complexes, on a recours, dans la plupart des
langages, à ce que l'on appelle un constructeur et un destructeur.
Ces deux éléments, qui portent pour une fois très bien leur nom, permettent
respectivement de créer et de supprimer un objet.
- Le constructeur est utilisé pour initialiser un objet : non seulement
l'objet en lui-même, mais également les valeurs des champs. En Pascal Objet,
lorsqu'on construit un objet, ses champs sont toujours initialisés à la valeur
0, false ou nil selon les cas. Le constructeur est une méthode très spéciale
souvent nommée create et appelée lors de la création d'un objet et qui permet
donc de fixer des valeurs spécifiques ou d'appeler des méthodes pour réaliser
cette tâche d'initialisation.
- Le destructeur est utilisé pour détruire un objet déjà créé à l'aide
du constructeur. L'objet est entièrement libéré de la mémoire, un peu à la manière
d'un élément pointé par un pointeur lorsqu'on applique un dispose sur le
pointeur. Le destructeur est souvent appelé destroy.
Le cycle de vie d'un objet est le suivant : on le crée à l'aide du constructeur, on
l'utilise aussi longtemps qu'on le veut, et on le détruit ensuite à l'aide du
destructeur. Notez qu'une classe n'a pas de cycle de vie puisque ce n'est qu'une
définition de structure, au même titre qu'une définition de type présente dans un
bloc type.
XVI-C. Bases de la programmation objet sous Delphi
Cette partie a pour but de vous faire découvrir les éléments de base de la
programmation objet sous Delphi, à savoir la déclaration de classes, de champs, de
méthodes, et d'objets. L'utilisation de ces éléments ainsi que du couple
constructeur/destructeur est également au programme.
XVI-C-1. Préliminaire : les différentes versions de Delphi
Lorsqu'on parle de programmation objets, on touche un domaine de prédilection du
langage Pascal Objet. Ce langage, intégré à Delphi depuis la version 1 et
descendant de Turbo Pascal, intègre, au fil des versions, de plus en plus de
nouveautés au fil de l'évolution des techniques de programmation objet. Ainsi, les
interfaces sont apparues récemment, dans la version 2 ou 3 (ma mémoire me joue un
tour à ce niveau, désolé). Autre exemple : la directive overload n'est apparue que
dans Delphi 5 alors que la notion sous-jacente existe dans d'autres langages depuis
longtemps.
Le but de cette petite introduction n'est pas de jeter la pierre à Pascal Objet qui
s'en tire bien dans le monde de la programmation objet, mais plutôt de vous mettre
en garde : selon la version de Delphi dont vous disposez, il se peut que certaines
des notions les plus pointues vues dans la suite de ce chapitre n'y soient pas
intégrées. Ce chapitre est actuellement conforme au langage Pascal Objet présent
dans la version 6 de Delphi, que je vous suggère d'acquérir si vous souhaitez
bénéficier des évolutions les plus récentes du langage.
XVI-C-2. Définition de classes
La partie que vous venez peut-être de lire étant assez théorique, vous vous sentez
peut-être un peu déconcerté. Il est maintenant temps de passer à la pratique,
histoire de tordre le cou à vos interrogations. Attaquons donc l'écriture d'un peu
de code Pascal Objet.
Une classe, en tant que type de donnée évolué, se déclare dans un bloc type.
Il n'est pas conseillé, même si c'est possible, de déclarer une classe dans un bloc
type local à une procédure ou fonction ; les deux endroits idéaux pour les
déclarations de classes sont donc dans un bloc type dans l'interface ou au
début de l'implémentation d'une unité.
La déclaration d'une classe prend la forme suivante :
NomDeClasse = class [(ClasseEtendue)]
déclarations_de_champs_ou_de_methodes
end;
|
Dans la déclaration ci-dessus, le nom de la classe doit être un identificateur, le
mot-clé class dénote une déclaration de classe. Il peut être suivi du nom
d'une autre classe entre parenthèses : dans ce cas la classe que vous déclarez
étend la classe en question et donc se base sur tout ce que contient la classe
étendue (nous y reviendrons plus tard). Ensuite, vous pouvez déclarer des champs et
des méthodes, puis la déclaration de classe se termine par un end suivi d'un
point-virgule.
Partons de l'exemple pris dans la partie précédente, à savoir celui d'une
télévision. Voici la déclaration de base de la classe "Television" :
A l'intérieur d'une déclaration de classe, il est possible de déclarer des champs
et des méthodes. Je profite de l'occasion pour mentionner un terme que vous
rencontrerez certainement dans l'aide de Delphi : celui de membre. On
appelle membre d'une classe un élément déclaré dans la classe, donc un champ
ou une méthode (nous verrons plus loin que cela comprend aussi les éléments appelés
propriétés). Un champ se déclare comme une variable dans une section
var. Voici la déclaration de la classe Television, complétée avec les champs
présentés sur le premier diagramme (celui ne faisant pas encore apparaître
l'appareil à écran) :
Television = class
poids: integer;
longueur_diagonale: integer;
allume: boolean;
chaines_memorisees: array[1..MAX_CHAINES] of integer;
end;
|
Comme vous pouvez le constater, dans l'état actuel, le morceau de code
ci-dessus ressemble furieusement à la déclaration d'un enregistrement (à tel
point qu'il suffirait de substituer le mot
record au mot
class
pour passer à un enregistrement). Nous allons maintenant déclarer les méthodes
de la classe Television. Une méthode se déclare comme pour une déclaration dans
l'interface d'une unité, à savoir la première ligne donnant le mot
procedure ou
fonction (nous parlons toujours de méthodes, même si
Pascal Objet, lui, parle encore de procédures ou de fonctions), le nom de la
méthode, ses paramètres et son résultat optionnel. Voici la déclaration
désormais complète de la classe Television (il est à noter que jusqu'ici vous
deviez pouvoir compiler l'application dans laquelle vous aviez inséré le code
source, ce qui ne va plus être le cas pendant un moment. Si vous avez des
soucis de compilation, consultez le code source complet
ici) :
Television = class
poids: integer;
longueur_diagonale: integer;
allume: boolean;
chaines_memorisees: array[1..MAX_CHAINES] of integer;
procedure allumer;
procedure eteindre;
procedure memoriser_chaine (numero, valeur: integer);
end;
|
Pour l'instant, ceci termine la déclaration de la classe. Si vous êtes très
attentif, vous devez vous dire : « mais quid du constructeur et du destructeur ? ».
La réponse est que nous n'avons pas à nous en soucier, mais vous le justifier va me
prendre un plus de temps. En fait, lorsque vous déclarez une classe en n'indiquant
pas de classe étendue, Pascal Objet prend par défaut la classe
"TObject", qui comporte déjà un constructeur et un destructeur. Comme la classe
Television étend la classe TObject, elle récupère tout ce que possédait "TObject"
(c'est à peine vrai, mais nous fermerons les yeux là-dessus pour l'instant, si vous
le voulez bien) et récupère donc entre autres le constructeur et le destructeur. Il
se trouve que ces deux méthodes particulières peuvent être utilisées pour la classe
Television, grâce à un mécanisme appelé héritage que nous détaillerons plus
tard, ce qui fait que nous n'écrirons pas de constructeur ni de destructeur pour
l'instant.
Si vous tentez de compiler le code source comprenant cette déclaration de classe,
vous devriez recevoir 3 erreurs dont l'intitulé ressemble à « Déclaration forward
ou external non satisfaite : Television.????? ». Ces messages signalent tout
simplement que vous avez déclaré 3 méthodes, mais que vous n'avez pas écrit le code
source des méthodes en question. Nous allons commencer par écrire le code source de
la méthode "allumer" de la classe "Television".
Une méthode se déclare dans la section implementation de l'unité dans
laquelle vous avez déclaré la classe. La syntaxe du code source de la méthode doit
être :
procedure|function NomDeClasse.NomDeMethode [(liste_de_parametres)] [: type_de_resultat];
[declarations]
begin
[instructions]
end;
|
Ne vous fiez pas trop à l'aspect impressionnant de ce qui précède. Le seul fait
marquant est que le nom de la méthode doit être préfixé par le nom de sa classe et
d'un point. Voici le squelette de la méthode "allumer" de la classe "Television" :
procedure Television.allumer;
begin
end;
|
Lorsque vous écrivez une méthode d'une classe, vous devez garder en tête que le
code que vous écrirez, lorsqu'il sera exécuté, concernera un objet de la classe que
vous développez. Ceci signifie que vous avez le droit de modifier les valeurs des
champs et d'appeler d'autres méthodes (nous allons voir dans un instant comment
faire), mais ce seront les champs et méthodes de l'objet pour lequel la méthode a
été appelée. Ainsi, si vous modifiez le champ "allume" depuis la méthode "allumer"
qui a été appelée pour l'objet "Tele1", le champ "allume" de l'objet "Tele2" ne
sera pas modifié.
Pour modifier la valeur d'un champ depuis une méthode, affectez-lui simplement une
valeur comme vous le feriez pour une variable. Voici le code source complet de la
méthode "allumer" :
procedure Television.allumer;
begin
allume := true;
end;
|
Comme vous pouvez le constater, c'est relativement simple. Pour vous entraîner,
écrivez vous-même le code source de la méthode "éteindre". Nous allons écrire
ensemble le code source de la troisième et dernière méthode, "memoriser_chaine".
Cette méthode prend deux paramètres, "numero" qui donne le numéro de chaîne et
"valeur" qui donne la valeur de réglage de la chaîne (nous nous en tiendrons là
pour l'émulation d'un téléviseur !). La partie squelette est toujours écrite
suivant le même modèle. Le code source est très simple à comprendre, mis à part le
fait qu'il agit sur un champ de type tableau, ce qui ne change pas grand-chose,
comme vous allez pouvoir le constater :
procedure Television.memoriser_chaine(numero, valeur: integer);
begin
if (numero >= 1) and (numero <= MAX_CHAINES) then
chaines_memorisees[numero] := valeur;
end;
|
Une fois le code source de ces trois méthodes entré dans l'unité, vous devez
pouvoir la compiler. Si ce n'est pas le cas, récupérez le code source
ici.
Vous savez maintenant ce qu'il est indispensable de connaître sur l'écriture des
classes, à savoir comment les déclarer, y inclure des champs et des déclarations de
méthodes, ainsi que comment implémenter ces méthodes. La suite de ce chapitre
abordera diverses améliorations et compléments concernant l'écriture des classes,
mais la base est vue. Nous allons maintenant passer à l'utilisation des objets, au
travers de petits exemples.
XVI-C-3. Déclaration et utilisation d'objets
Vous avez vu dans la partie précédente (ou alors vous le saviez déjà) qu'une
classe ne s'utilise pas directement : on passe par des objets instances de
cette classe. L'utilisation des objets ayant déjà été abordée au chapitre 10,
je vais être rapide sur les notions de construction et de destruction ainsi que
sur le moyen d'appeler une méthode ou de modifier un champ. Ce paragraphe va
surtout consister en l'étude de quelques exemples ciblés qui vous permettront
de vous remettre ces opérations en mémoire.
Pour créer un objet, on doit faire appel au constructeur sous une forme
particulière :
Objet := Classe.create [(parametres)];
Pour appeler une méthode, on utilise la syntaxe :
Objet.Methode [(paramètres)];
On peut faire référence à un champ par la construction Objet.Champ . Enfin,
la destruction consiste à appeler le destructeur, appelé communément destroy, comme
on le fait pour une méthode. Un objet se déclare dans une section var, avec
comme nom de type la classe de cet objet. Voici une petite démo qui effectue
quelques petits traitements sur un objet et affiche quelques résultats :
procedure TForm1.Button1Click(Sender: TObject);
var
Tele1: Television;
begin
Tele1 := Television.Create;
Tele1.poids := 35;
Tele1.longueur_diagonale := 70;
Tele1.allumer;
if Tele1.allume then
ShowMessage('Télévision allumée')
else
ShowMessage('Télévision éteinte');
Tele1.eteindre;
if Tele1.allume then
ShowMessage('Télévision allumée')
else
ShowMessage('Télévision éteinte');
Tele1.memoriser_chaine(1, 2867);
ShowMessage('La chaîne n°1 est réglée sur la valeur '+IntToStr(Tele1.chaines_memorisees[1]));
Tele1.Destroy;
end;
|
Nous allons maintenant réaliser une petite application qui va nous permettre de
manipuler à souhait un objet de classe "Television". Vous aurez ainsi l'occasion de
voir comment on peut maîtriser le cycle de vie d'un objet, à savoir
construction-utilisation-destruction. Nous aurons également l'occasion d'ajouter
des champs et des méthodes à la classe définissant la fiche principale, ce qui vous
fera manipuler beaucoup de notions vues jusqu'à présent. Commencez par créer
l'interface de la fiche principale d'une nouvelle application : téléchargez
le projet avec
juste l'interface, ou bien créez-là vous-même à l'aide des deux captures
ci-dessus dont l'une donne les noms des composants. Le principe de l'interface est
simple : on peut créer ou détruire une télévision, et l'éditer lorsqu'elle est
créée. Lorsque la télévision est en plus allumée, on peut obtenir la liste des
chaînes et les mémoriser.


La première chose à faire est d'intégrer la déclaration de la classe "Television"
et le code source des 3 méthodes de cette classe. Assurez-vous d'inclure la
déclaration de la classe "Television" AVANT celle de la classe "TfmPrinc" (eh bien
oui, c'est une déclaration de classe, non ?). Utilisez les fragments de code source
donnés dans la section précédente pour cela. Déclarez la constante MAX_CHAINES avec
la valeur 10. Fixez les propriétés "Visible" des panels "pnTele" et "pnChaines" à
false, ainsi que la propriété "Enabled" des deux boutons "btDetrTele" et
"btEteindre" à false. Faites fonctionner le bouton "Quitter" (en mettant un simple
Close; dans la procédure de réponse au clic).
Déclarez ensuite un objet "Tele1" de classe "Television" ; vous pourriez le placer
dans le bloc var de l'interface de l'unité, mais nous pouvons le placer à un
endroit plus stratégique. En effet, vous pouvez remarquer que le code source
contient la déclaration de la classe "TfmPrinc". Si vous avez lu le chapitre 10,
vous savez que cette classe est celle qui permet de définir la fiche. Nous avons
tout à fait le droit d'ajouter des éléments à cette classe, et nous allons
justement y ajouter un champ... Tele1. Attention de ne pas tout confondre : "Tele1"
est un objet de classe "Television", mais ce sera également un champ de la classe
"TfmPrinc" (il est tout à fait permis d'utiliser des objets en tant que champs).
Voici un morceau de code source pour vous aider :
...
btChangerReglage: TButton;
btQuitter: TButton;
private
Tele1: Television;
public
...
|
L'étape suivante consiste à permettre de construire et détruire l'objet depuis
l'interface de l'application. Lorsque l'objet sera créé, l'interface affichera le
panneau "pnTele" qui permet le contrôle de la télévision. La procédure de réponse
au clic sur le bouton de création va devoir créer l'objet "Tele1" en appelant le
constructeur de la classe "Television", afficher le panel "pnTele" et mettre son
contenu à jour. Il est également nécessaire de désactiver le bouton de création
pour ne pas recréer l'objet une autre fois avant de l'avoir détruit ; pour pouvoir
le détruire, on doit également activer le bouton de destruction. Voici le code
source de la procédure de réponse au clic :
procedure TfmPrinc.btCreerTeleClick(Sender: TObject);
begin
btCreerTele.Enabled := False;
btDetrTele.Enabled := True;
Tele1 := Television.Create;
pnTele.Visible := true;
MajTele;
end;
|
Juste une remarque à propos de l'accès au champ "Tele1" : vous le manipulez depuis
une méthode (même si pour l'instant, nous parlions de procédures, ce qui ne sera
PLUS le cas désormais) de la même classe, ce qui fait que vous y avez accès
directement, comme si c'était une variable globale. Il nous reste à écrire une
méthode "MajTele" qui mettra à jour le contenu du panel "pnTele". J'ai bien dit
méthode car nous allons écrire une procédure faisant partie de la classe TfmPrinc
(donc une méthode). Voici sa déclaration dans celle de la classe "TfmPrinc" :
...
private
Tele1: Television;
procedure MajTele;
public
...
|
Cette procédure doit mettre à jour les deux zones d'édition de poids et de
diagonale, activer ou non les boutons d'allumage/extinction et afficher/masquer le
panel pnChaines si la télévision n'est pas allumée. Si la télévision est allumée,
nous ferons appel à une autre méthode appelée MajChaines pour mettre à jour le
contenu du panneau de visualisation des chaînes. Voici le code source de "MajTele" :
procedure TfmPrinc.MajTele;
begin
edPoids.Text := IntToStr(Tele1.poids);
edDiago.Text := IntToStr(Tele1.longueur_diagonale);
btAllumer.Enabled := not Tele1.allume;
btEteindre.Enabled := Tele1.allume;
pnChaines.Visible := Tele1.allume;
if Tele1.allume then
MajChaines;
end;
|
La méthode MajChaines doit être écrite de la même manière que "MajTele"
(déclarez-là vous-même dans la déclaration de la classe "TfmPrinc"). Elle vide puis
remplit la zone de liste "lbChaines" avec les chaînes mémorisées dans l'objet
"Tele1". De plus, elle active le premier élément de la liste afin d'activer son
édition (nous nous occuperons de cela plus tard). Voici son code source, assez
simple, qui fait appel au champ "chaines_memorisees" de "Tele1" :
procedure TfmPrinc.MajChaines;
var
i: integer;
begin
lbChaines.ItemIndex := -1;
lbChaines.Items.Clear;
for i := 1 to MAX_CHAINES do
lbChaines.Items.Add('Chaine '+IntToStr(i)+' : '+IntToStr(Tele1.chaines_memorisees[i]));
end;
|
Il est facile de faire fonctionner le bouton de destruction de la télévision. Un
clic sur ce bouton doit cacher l'interface d'édition de la télévision et détruire
l'objet "Tele1". Voici son code source :
procedure TfmPrinc.btDetrTeleClick(Sender: TObject);
begin
btDetrTele.Enabled := false;
btCreerTele.Enabled := true;
pnTele.Visible := false;
Tele1.Destroy;
end;
|
L'étape suivante consiste maintenant à faire fonctionner les deux boutons "Fixer"
qui permettent respectivement de fixer le poids et la diagonale de la télévision. A
chaque fois, on regarde le contenu de la zone d'édition associée et on essaie de
construire une valeur entière. En cas de réussite cette valeur est affectée au
champ correspondant de l'objet "Tele1" et en cas d'échec la valeur en cours est
rétablie dans la zone d'édition. les deux méthodes de réponse aux clics sont très
similaires, voici l'une des deux, écrivez l'autre vous-même à titre d'exercice :
procedure TfmPrinc.btFixePoidsClick(Sender: TObject);
var
V, E: integer;
begin
Val(edPoids.Text, V, E);
if (E = 0) then
Tele1.poids := V
else
begin
ShowMessage(edPoids.Text + ' n''est pas une valeur entière acceptable');
edPoids.Text := IntToStr(Tele1.poids);
end;
end;
|
Il est maintenant temps de faire fonctionner les deux boutons servant à allumer et
à éteindre la télévision. Ces deux boutons fonctionnent suivant le même principe
que les deux boutons de création/destruction, à savoir qu'ils s'excluent
mutuellement, et que le bouton d'allumage doit afficher et mettre à jour le panel
d'édition des chaînes. Voici le code source des 2 méthodes de réponse au clic sur
ces deux boutons, qui n'ont rien de bien difficile :
procedure TfmPrinc.btAllumerClick(Sender: TObject);
begin
btAllumer.Enabled := false;
btEteindre.Enabled := true;
pnChaines.Visible := true;
MajChaines;
end;
procedure TfmPrinc.btEteindreClick(Sender: TObject);
begin
pnChaines.Visible := false;
btAllumer.Enabled := true;
btEteindre.Enabled := false;
end;
|
Il ne nous reste plus, pour terminer cette petite application fort inutile (!),
qu'à faire fonctionner la mémorisation des chaînes et à régler un ou deux petits
détails. La liste des chaînes doit fonctionner selon le principe suivant :
l'utilisateur sélectionne une chaîne dans la liste, ce qui affiche le réglage dans
la zone d'édition de droite. Le fait de modifier cette valeur puis de cliquer sur
"Changer Réglage" met à jour la valeur dans l'objet "Tele1" ainsi que dans la liste
des chaînes. Etant donné que l'application que nous développons est très simples,
nous irons à l'essentiel sans prendre de détours ni trop de précautions,
l'essentiel étant de vous montrer encore une fois comment utiliser l'objet
"Tele1".
La première chose à faire est de réagir à un clic sur la liste pour gérer le
changement de sélection (lorsque la sélection est modifiée au clavier et non à la
souris, l'événement OnClick se produit tout de même, ce qui fait que nous pouvons
utiliser cet événement pour gérer les changements de sélection par l'utilisateur).
Lors d'un changement de sélection, le numéro de l'élément sélectionné donne un
index permettant d'obtenir la valeur du réglage de la chaîne correspondante. Cette
valeur doit alors être affichée dans la zone d'édition. Voici le code source de la
méthode de réponse au clic sur la zone de liste :
procedure TfmPrinc.lbChainesClick(Sender: TObject);
begin
if (lbChaines.ItemIndex >= 0) then
edValeur.Text := IntToStr(Tele1.chaines_memorisees[lbChaines.ItemIndex + 1])
else
edValeur.Text := '';
end;
|
Juste une remarque sur le code ci-dessus : le numéro d'élément sélectionné (donné
par ItemIndex) commence à 0, or les éléments du tableau de chaînes sont indexés à
partir de 1, d'où le décalage. Méfiez-vous de ce genre de petit piège très
fréquent. Passons enfin à la réaction au clic sur le bouton "btChangerReglage". Ce
bouton est toujours actif, ce qui nous contraint à effectuer quelques petits tests.
En effet, si l'utilisateur clique sur ce bouton alors qu'aucune chaîne n'est
sélectionnée, il faudra lui expliquer qu'il doit d'abord en sélectionner une avant
de pouvoir modifier son réglage. Voici le code source (un plus imposant) de la
méthode en question :
procedure TfmPrinc.btChangerReglageClick(Sender: TObject);
var
V, E: integer;
begin
if (lbChaines.ItemIndex >= 0) then
begin
Val(edValeur.Text, V, E);
if (E = 0) then
begin
Tele1.memoriser_chaine(lbChaines.ItemIndex + 1, V);
lbChaines.Items[lbChaines.ItemIndex] :=
'Chaine ' + IntToStr(lbChaines.ItemIndex + 1) + ' : ' +
IntToStr(Tele1.chaines_memorisees[lbChaines.ItemIndex + 1]);
end
else
begin
ShowMessage(edValeur.Text + ' n''est pas une valeur entière acceptable');
edValeur.Text := IntToStr(Tele1.chaines_memorisees[lbChaines.ItemIndex + 1]);
end;
end
else
begin
ShowMessage('Vous devez d''abord sélectionner une chaîne avant de modifier son réglage');
edValeur.Text := '';
end;
end;
|
Le développement de cette petite application exemple est désormais terminé. Il y
aurait encore des améliorations à voir, comme vérifier si "Tele1" a bel et bien été
détruit avant de quitter l'application (ce n'est pas trop grave puisque Windows
récupèrera la mémoire tout seul). Il serait également intéressant de filtrer les
valeurs négatives pour le poids et la longueur de diagonale. Libre à vous d'essayer
d'intégrer ces améliorations au projet. Vous pouvez télécharger le code source
complet
sans ou
avec
ces améliorations (attention, à partir de maintenant, les codes source
téléchargeables sont créés avec Delphi 6, ce qui ne devrait pas poser de problème
avec Delphi 5. Pour les versions antérieures, je suis dans l'incapacité d'effectuer
les tests, désolé).
Nous allons maintenant compléter notre implémentation de la classe "Television" en
réalisant l'implémentation correspondant au schéma incluant l'appareil à écran.
Tout d'abord, il nous faut remarquer les différences avec l'ancien schéma pour
faire l'adaptation du code source en douceur.
- Il nous faut une classe "AppareilAEcran", qui va prendre quelques-uns des
champs et méthodes de la classe "Television" ;
- "Television" doit étendre la classe "AppareilAEcran"
- Il nous faut une classe "EcranOrdinateur"...
Commençons par écrire la déclaration de la classe "AppareilAEcran". Elle n'étend
aucune classe (explicitement, car comme je l'ai déjà dit, elle étendra "TObject"),
a comme champs "allume", "poids" et "diagonale" et comme méthodes "allumer" et
"eteindre". Essayez d'écrire vous-même la déclaration de cette classe (sans le code
source des méthodes), puis regardez la correction ci-dessous :
AppareilAEcran = class
poids: integer;
longueur_diagonale: integer;
allume: boolean;
procedure allumer;
procedure eteindre;
end;
|
L'écriture de cette partie ne doit pas vous avoir posé trop de problèmes, puisqu'il
suffisait de calquer la déclaration sur celle de la classe "Television". Ecrivez
maintenant le code source des méthodes de la classe "AppareilAEcran" (petit
raccourci : placez-vous à l'intérieur de la déclaration de la classe
"AppareilAEcran", et utilisez le raccourci clavier Ctrl+Shift+C, qui doit vous
générer le squelette de toutes les méthodes). Voici ce que cela doit donner :
procedure AppareilAEcran.allumer;
begin
allume := true;
end;
procedure AppareilAEcran.eteindre;
begin
allume := false;
end;
|
Dans l'état actuel des choses, nous avons deux classes, "AppareilAEcran" et
"Television", complètement indépendantes. Nous voudrions bien faire en sorte que
"Television" étende "AppareilAEcran". Pour cela, il va falloir procéder à quelques
modifications. La manoeuvre de départ consiste à dire explicitement que
"Television" étend "AppareilAEcran". Pour cela, modifiez la première ligne de la
déclaration de la classe "Television" en ceci :
Television = class(AppareilAEcran)
|
Une fois cette première manipulation effectuée, les deux classes sont bien reliées
entre elles comme nous le voulions, cependant, nous avons déclaré deux fois les
champs et méthodes communs aux deux classes, ce qui est une perte de temps et de
code source, et surtout une erreur de conception. Rassurez-vous, ceci était
parfaitement délibéré, histoire de vous montrer que l'on peut retirer une partie du
code qui est actuellement écrit.
Pour preuve, si vous compilez actuellement le projet comprenant la déclaration des
2 classes et de leurs méthodes, aucune erreur ne se produira. Cependant, nous
allons maintenant profiter d'une des fonctionnalités les plus intéressantes de la
programmation objet : l'héritage. Comme "Television" étend "AppareilAEcran", elle
hérite de tout le contenu de cette classe. Ainsi, le fait de redéfinir les
champs "allume", "poids", "longueur_diagonale" et les méthodes "allumer" et
"eteindre" dans la classe "Television" est une redondance. Vous pouvez donc
supprimer ces déclarations. Faites le et supprimez également le code source des
deux méthodes "allumer" et "eteindre" de la classe "Television" (supprimez les deux
méthodes dans la partie implementation). Voici la nouvelle déclaration de la
classe "Television" :
Television = class(AppareilAEcran)
chaines_memorisees: array[1..MAX_CHAINES] of integer;
procedure memoriser_chaine (numero, valeur: integer);
end;
|
Si vous compilez à nouveau le code source, vous ne devez avoir aucune erreur de
compilation. A titre de vérification, vous pouvez consulter le
code
source dans la version actuelle ici (c'est devenu une unité à part entière).
Vous vous demandez peut-être encore comment tout cela peut fonctionner. Eh bien en
fait, comme la classe "Television" étend la classe "AppareilAEcran", elle hérite de
tous ses éléments, et donc des fonctionnalités implémentées dans cette classe.
Ainsi, il est possible d'allumer et d'éteindre une télévision, car une télévision
est avant tout un appareil à écran auquel on a ajouté des particularités.
Nous allons maintenant implémenter la classe "EcranOrdinateur" à partir de la
classe "AppareilAEcran". Voici la déclaration de base de cette classe :
EcranOrdinateur = class(AppareilAEcran)
end;
|
Si vous regardez un peu la liste des éléments introduits dans la classe
"EcranOrdinateur", vous constatez qu'il est question de résolutions. Pour définir
une résolution, nous allons utiliser un enregistrement dont voici la définition :
TResolution = record
hauteur,
largeur: word;
end;
|
Les deux champs de la classe sont assez simples, il s'agit d'un élément de type
"TResolution" (déclaré ci-dessus), et d'un tableau dynamique d'éléments de type
"TResolution". La seule méthode, permettant de fixer la résolution actuelle,
accepte un paramètre de type résolution et doit vérifier avant de l'appliquer
qu'elle fait partie des résolutions possibles. Voici la déclaration complète de la
classe "EcranOrdinateur" :
EcranOrdinateur = class(AppareilAEcran)
resolutions_possibles: array of TResolution;
resolution_actuelle: TResolution;
function changer_resolution(NouvRes: TResolution): boolean;
end;
|
L'implémentation de la méthode "changer_resolution" consiste à parcourir le tableau
des résolutions possibles, et dés que la résolution a été trouvée, elle est
appliquée, sinon, elle est rejetée. le résultat indique si le changement a été oui
ou non accepté. Voici le code source de cette méthode, qui manipule les deux champs
introduits dans la classe "EcranOrdinateur" :
function EcranOrdinateur.changer_resolution(NouvRes: TResolution): boolean;
var
i: integer;
begin
i := 0;
result := false;
while (i < length(resolutions_possibles)) and not result do
if resolutions_possibles[i].hauteur = NouvRes.hauteur and
resolutions_possibles[i].Largeur then
begin
resolution_actuelle := NouvRes;
result := true;
end;
end;
|
Et c'en est fini des déclarations. La classe "EcranOrdinateur" étendant la classe
"AppareilAEcran", elle hérite de tous ses éléments, et chaque objet instance de la
classe "EcranOrdinateur" peut donc être allumé ou éteint (en tant qu'appareil à
écran particulier).
Ceci termine ce long point sur la création et la manipulation des objets. Je vous
engage à réaliser une petite application ressemblant à celle créée pour la classe
"Television", et à faire fonctionner un "écran d'ordinateur". Vous pouvez également
remplacer la définition de "Television" (dans l'application que nous avons créé un
peu avant) par la définition complète des trois classes. Vous verrez que tout
fonctionne exactement comme si vous n'aviez effectué aucun changement.
XVI-C-4. Utilisation d'un constructeur et d'un destructeur, notions sur l'héritage
Créer un objet peut parfois nécessiter des actions spéciales, comme
l'initialisation de valeurs de champs ou l'allocation de mémoire pour certains
champs de l'objet. Ainsi, si vous utilisez des champs de type pointeur, vous aurez
probablement besoin d'allouer de la mémoire par des appels à new. Vous pouvez
également avoir besoin de fixer des valeurs directement à la construction, sans
avoir à les respécifier plus tard. Lorsque ce genre de besoin apparaît, il faut
systématiquement penser « constructeur ».
Le constructeur, comme vous l'avez peut-être lu dans la première partie du
chapitre, est une méthode spéciale qui est exécutée au moment de la construction
d'un objet. Pour la petite histoire, ce n'est pas une vraie méthode, puisqu'on ne
l'appelle pas à partir d'un objet mais d'une classe. C'est ce qu'on appelle une «
méthode de classe ». Cette méthode de classe effectue en fait elle-même la création
physique de l'objet en mémoire, et le retourne indirectement (pas en tant que
résultat de méthode, puisque le constructeur n'est pas une méthode), ce qui permet
une affectation du "résultat" de l'appel du constructeur. La seule chose que vous
ayez à retenir ici, c'est que grâce au mécanisme appelé héritage dont nous
avons vu une partie de la puissance, vous pouvez écrire votre propre constructeur
(voire plusieurs), faisant appel au constructeur système pour lui déléguer les
tâches difficiles de création de l'objet, et réalisant ensuite autant
d'actions que vous le désirez, et en plus des tâches systèmes de construction ayant
été réalisées auparavant. L'objet récupéré lors d'un appel à ce
constructeur personnalisé aura alors déjà subi l'action du constructeur
système et l'action de toutes les instructions que vous aurez placé dans votre
constructeur personnalisé.
Comme un exemple fait souvent beaucoup de bien après un grand discours comme
celui-ci, voici la toute première version du constructeur que nous pourrions écrire
pour la classe "MachineAEcran" :
constructor AppareilAEcran.Create;
begin
allume := false;
poids := 20;
longueur_diagonale := 55;
end;
|
Et voici sa déclaration dans celle de la classe "AppareilAEcran" :
AppareilAEcran = class
...
procedure eteindre;
constructor Create;
end;
|
Un constructeur se déclare non pas à l'aide d'un des mots réservés procedure
ou function, mais à l'aide du mot-clé réservé à cet usage :
constructor. Vient ensuite le nom du constructeur ; le nom classique à
conserver dans la mesure du possible est « Create ». Peut éventuellement suivre une
liste de paramètres qui est absente dans le cas présent.
Il est possible de définir un destructeur de la même manière, à une petite
difficulté près, nous allons le voir. Le destructeur est une méthode classique
déclarée d'une manière spécifique, et qui est appelée afin de détruire l'objet
duquel on a appelé le destructeur. Le nom habituel du destructeur est "Destroy", et
c'est une méthode sans paramètre. Voici comment on déclare un destructeur dans la
déclaration d'une classe :
destructor Destroy; override;
|
La partie destructor Destroy; permet de dire qu'on définit un destructeur
personnalisé pour la classe en cours de déclaration. Ce destructeur est introduit
par le mot réservé destructor suivi du nom, traditionnellement "Destroy",
puis d'un point-virgule puisque le destructeur n'a normalement pas de paramètre. La
suite, à savoir « override », est une nouveauté et nous allons y consacrer
toute notre attention.
Ce mot-clé, suivant la déclaration du destructeur et lui-même suivi
immédiatement d'un point-virgule signale que nous appliquons une
surcharge à une méthode déjà existante (j'ai lâché là un des gros mots
de la programmation objet). Ce terme de surchargesignifie que la méthode
que nous allons définir va s'appuyer sur la méthode de même nom, mais déclarée
dans la classe parente. Ainsi, la classe parente de "AppareilAEcran", à savoir
"TObject", possède déjà un destructeur appelé "Destroy". Nous allons donc
redéfinir une méthode qui existe déjà, mais en étendant ses possibilités. Dans
la grande majorité des cas, ceci permet d'adapter le fonctionnement à la
nouvelle classe en gérant ce qui a été ajouté à la classe parente : Il est
alors possible d'écrire les instructions spécifiques à l'extension de la classe
parente, puis de déléguer le reste du travail à réaliser par cette méthode, à
savoir le traitement des données de base de la classe parente (en appelant la
méthode de même nom de la classe parente afin de permettre le traitement qui y
était effectué par cette même méthode).
Je m'explique : TObject possède une méthode (plus exactement un destructeur) appelé
Destroy, qui est parfaitement adaptée à TObject. Si nous créons une nouvelle classe
dont les objets manipulent des pointeurs, il serait sage qu'à la destruction de
chaque objet, la mémoire allouée aux pointeurs soit du même coup libérée. Il serait
tout à fait possible d'écrire une méthode autre que le destructeur qui réaliserait
cela, mais il n'y aurait aucun moyen de s'assurer que la méthode en question soit
appelée quand on détruit un objet (oubli ou mauvaise manipulation par exemple). Le
destructeur est l'endroit idéal où placer ces instructions, mais là où le
constructeur nous facilitait la tâche en nous permettant d'écrire des instructions
sans nous préoccuper du constructeur système, le destructeur est plus vache et on
doit explicitement effectuer les tâches manuelles liées à la destruction, mais
aussi faire en sorte que le destructeur de la classe parente soit appelé (ce ne
sera pas le cas par défaut !).
Voici le code source de base du destructeur déclaré plus haut :
destructor AppareilAEcran.Destroy;
begin
inherited;
end;
|
Plusieurs choses sont à remarquer dans cet extrait de code source : on utilise dans
la ligne de déclaration non pas procedure ou function mais
destructor. Suit ensuite la déclaration classique d'une méthode. Notez
l'absence du mot-clé override : ce mot-clé ne doit être présent que dans la
déclaration de classe. Le corps du destructeur réserve une petite surprise : la
seule instruction est un simple « inherited; ». Cette instruction signifie :
"Appeler la méthode de même nom, déclarée dans la classe parente de celle que je
suis en train d'implémenter". inherited appelle donc ici le destructeur
Destroy de la classe "TObject", ce qui permettra de détruire l'objet en cours lors
d'un appel à son destructeur, mais également de réaliser d'autres tâches
avant (il serait impossible et quelque peu... bête de vouloir appliquer des
instructions à un objet dont le coeur aurait déjà été détruit).
La programmation objet ayant parmi ses objectifs la réutilisation de code existant
sans avoir à le réécrire, le fait de surcharger une méthode permet de
reprendre avantageusement toute un lot d'instructions sans les reprogrammer
puisqu'elles sont présentes dans la classe parente de la classe qu'on est en train
de programmer. Cette surcharge, qui est l'un des aspects majeurs d'une
notion plus vaste – l'héritage – est une manière d'étendre à chaque fois que
nécessaire, lorsqu'on crée une classe à partir d'une autre, les méthodes qui le
nécessitent, ce qui rend possible l'extension du code source des méthodes au fur et
à mesure qu'on passe d'une classe parente à une classe dérivée en augmentant les
possibilités des classes en question à chaque étape.
Revenons au code source du destructeur de la classe AppareilAEcran : comme il vaut
mieux s'assurer qu'un appareil à écran est éteint avant de le détruire (précaution
élémentaire, n'est-il pas ?), nous allons fixer la valeur du champ "allume" d'un
objet destiné à être détruit à faux avant de procéder à la destruction proprement
dite. Comme il a été mentionné plus tôt, il est impératif de réaliser cette
opération d'abord et de poursuivre par le destruction faite dans la classe
parente ensuite. Voici le code source mis à jour :
destructor AppareilAEcran.Destroy;
begin
allume := false;
inherited;
end;
|
Il y a peu à dire sur ce code source : remarquez encore une fois que les nouvelles
instructions, dans le cas du destructeur, doivent être obligatoirement effectuées
avant l'appel à la méthode de même nom héritée (inherited...). Notez que pour les
autres méthodes que le destructeur, ce sera généralement l'inverse : on effectue
d'abord les traitements de base par un appel à la méthode de même nom héritée, et
on effectue ensuite les traitements à ajouter pour prendre en compte les éléments
ajoutés à la classe parente. Nous allons tout de suite voir un exemple concret, en
écrivant un constructeur pour chacune des deux classes descendantes de la classe
"AppareilAEcran".
Pour ce qui concerne les télévisions, il serait intéressant d'initialiser
l'ensemble des chaînes au réglage 0. Les écrans d'ordinateurs, eux, pourraient
définir quelques résolutions autorisées systématiquement, comme le 640 x 480. Le
fait d'écrire ces deux constructeurs va non seulement vous montrer l'intérêt de
l'héritage, mais va aussi vous permettre de découvrir une nouvelle notion, celle de
méthode virtuelle, autre notion connexe à l'héritage.
En ce qui concerne la classe "Television", voici la déclaration du nouveau
constructeur :
constructor Television.Create;
var
i: integer;
begin
for i := 1 to MAX_CHAINES do
chaines_memorisees[i] := 0;
end;
|
Ce constructeur n'est qu'à moitié intéressant : il permet bien d'initialiser les
champs de l'objet qui vient d'être créé, mais il ne fait pas appel au constructeur
défini dans la classe "AppareilAEcran", ce qui fait que par défaut, vu notre
implémentation actuelle de ce constructeur, une télévision sera, par défaut,
éteinte, avec un poids de 0 kg (les champs d'un objet sont fixés par défaut à la
valeur nulle de leur type (soit 0 pour les entiers, nil pour les pointeurs, '' pour
les chaînes...) et une longueur de diagonale de 0 cm, ce qui est regrettable. Nous
allons donc modifier le constructeur et sa déclaration pour qu'il fasse appel au
constructeur de la classe "AppareilAEcran". Voici la déclaration modifiée :
...
procedure memoriser_chaine (numero, valeur: integer);
constructor Create; override;
end;
...
|
et le code source, qui fait appel au constructeur hérité (méthode Create de même
nom dans la classe parente) (n'essayez pas de compiler le code complet avec
l'extrait ci-dessous, un oubli tout à fait délibéré provoquant une erreur a été
fait afin de vous expliquer une nouvelle notion) :
constructor Television.Create;
var
i: integer;
begin
inherited;
for i := 1 to MAX_CHAINES do
chaines_memorisees[i] := 0;
end;
|
Comme il est précisé en remarque ci-dessus, si vous tentez de compiler le code
source modifié à l'aide du code source ci-dessus (si vous n'avez pas suivi, vous
pouvez le
télécharger
dans son état actuel ici), une erreur se produit, vous indiquant qu'il est «
impossible de surcharger une méthode statique ». Mais bon sang mais c'est bien sûr
!
Lorsqu'une méthode est écrite sans y ajouter le mot-clé
override (ou un ou
deux autres que nous allons voir tout de suite), elle est dite
statique,
c'est-à-dire qu'elle n'est pas candidate à la surcharge. Ainsi, il ne sera pas
possible de la surcharger (mais de la
redéfinir, nous y reviendrons). Bref,
notre premier constructeur a été défini sans ce mot-clé, et il est donc statique,
et ne peut donc pas être surchargé. Qu'à cela ne tienne, il va falloir faire en
sorte qu'il accepte la surcharge. Pour cela, on ne va pas utiliser le mot-clé
override car le constructeur de AppareilAEcran ne surcharge aucune autre
méthode, mais plutôt le mot-clé
virtual destiné précisément à cet usage. On
dira alors que le constructeur de la classe "AppareilAEcran" est virtuel,
c'est-à-dire qu'il est susceptible d'être surchargé dans une classe ayant
"AppareilAEcran" comme classe parente. Voici la déclaration modifiée du premier
constructeur :
...
procedure eteindre;
constructor Create; virtual;
destructor Destroy; override;
end;
...
|
Si vous compilez le code avec la nouvelle déclaration du constructeur, tout se
passe bien, maintenant que le premier constructeur est déclaré virtuel,
c'est-à-dire apte à être surchargé, et que le second est déclaré comme surchargeant
la méthode héritée, qui peut bien être surchargée. Que se passerait-il si nous
définissions une classe ayant "Television" comme classe parente et si nous
décidions d'y inclure un constructeur faisant appel à celui de la classe
"Television" ? Nous devrions lui adjoindre le mot-clé override, signalant
que nous surchargeons une méthode héritée. En effet, le mot-clé override
signifie non seulement que l'on va surcharger une méthode, mais que cette surcharge
pourra elle-même être surchargée.
Le fait d'écrire un constructeur pour la classe "Television" fait que la création
de tout objet de cette classe se fera en plusieurs étapes. Dans un premier temps,
c'est la méthode héritée qui est appelée par inherited. Ce constructeur
hérité fait lui-même appel implicitement appel au constructeur système de la classe
"TObject", puis initialise certains champs. Enfin, de retour dans le constructeur
de la classe "Television", les chaînes sont initialisées après que le reste des
champs de l'objet aient été initialisés et que l'objet lui-même ait été
construit.
Nous aurons l'occasion de voir d'autres exemples d'utilisation des méthodes
virtuelles et de la surcharge, et nous reparlerons également du mécanisme
d'héritage plus en détail dans un prochain paragraphe. Pour l'instant, je vais
passer à un tout autre sujet plus facile, histoire de ne pas trop vous démoraliser.
XVI-C-5. Visibilité des membres d'une classe