Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUMS DELPHI F.A.Q DELPHI TUTORIELS DELPHI LIVRES COMPOSANTS SOURCES DEFI TELECHARGEZ DELPHI TV

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

 Television = class
  end;
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
  // construction de l'objet Tele1
  Tele1 := Television.Create;
  // modification de champs
  Tele1.poids := 35;
  Tele1.longueur_diagonale := 70;
  // appel d'une méthode
  Tele1.allumer;
  // affichage d'infos
  if Tele1.allume then
    ShowMessage('Télévision allumée')
  else
    ShowMessage('Télévision éteinte');
  // encore un petit appel de méthode
  Tele1.eteindre;
  // et réaffichage d'infos.
  if Tele1.allume then
    ShowMessage('Télévision allumée')
  else
    ShowMessage('Télévision éteinte');
  // essai de mémorisation d'une chaîne
  Tele1.memoriser_chaine(1, 2867);
  // affichage du résultat
  ShowMessage('La chaîne n°1 est réglée sur la valeur '+IntToStr(Tele1.chaines_memorisees[1]));
  // destruction de Tele1
  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
    { Déclarations privées }
    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
    { Déclarations privées }
    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 :

  ...
    { dans la classe AppareilAEcran }
    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