Developpez.com

Plus de 2 000 forums
et jusqu'à 5 000 nouveaux messages par jour

Guide Pascal et Delphi


précédentsommaire

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 :

Image non disponible

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

Image non disponible

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 :

Image non disponible

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

Image non disponible

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 :

Image non disponible

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 :

 
Sélectionnez

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

 
Sélectionnez

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

 
Sélectionnez

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

 
Sélectionnez

 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 :

 
Sélectionnez

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

 
Sélectionnez

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

 
Sélectionnez

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 :

 
Sélectionnez

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 :

 
Sélectionnez

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.

Image non disponible
Image non disponible

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 :

 
Sélectionnez

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

 
Sélectionnez

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

 
Sélectionnez

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

 
Sélectionnez

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

 
Sélectionnez

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 :

 
Sélectionnez

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 :

 
Sélectionnez

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 :

 
Sélectionnez

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 :

 
Sélectionnez

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 :

 
Sélectionnez

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 :

 
Sélectionnez

 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 :

 
Sélectionnez

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 :

 
Sélectionnez

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

 
Sélectionnez

 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 :

 
Sélectionnez

  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 :

 
Sélectionnez

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

 
Sélectionnez

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

 
Sélectionnez

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

 
Sélectionnez

constructor AppareilAEcran.Create;
begin
  allume := false;
  poids := 20;
  longueur_diagonale := 55;
end;

Et voici sa déclaration dans celle de la classe "AppareilAEcran" :

 
Sélectionnez

 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 :

 
Sélectionnez

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 :

 
Sélectionnez

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 :

 
Sélectionnez

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 :

 
Sélectionnez

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 :

 
Sélectionnez

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

 
Sélectionnez

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 :

 
Sélectionnez

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

Tout ce que nous avons déclaré jusqu'à présent comme faisant partie d'une classe, que ce soit des champs, des méthodes, un constructeur ou un destructeur (auxquels s'ajouterons plus tard les propriétés) constitue ce qu'on appelle communément les membres de cette classe. On entend par membre un élément déclaré dans une classe, que ce soit un élément normal comme un champ, une méthode... ou même une méthode surchargeant une méthode héritée.

Jusqu'à maintenant, l'ensemble des membres de toutes nos classes avaient une caractéristique dont nous ne nous sommes pas préoccupé vu la petite taille des classes que nous manipulions. Le moment est venu de parler de cette caractéristique que vous avez peut-être remarquée : tous les membres de nos classes sont librement accessibles depuis l'extérieur des objets de ces classes. Ceci ne pose pas trop de problèmes tant qu'on manipule de toutes petites classes, ou qu'on se limite à une utilisation strictement limitée du code source qu'on écrit.

La programmation objet met à votre disposition le moyen de masquer certains éléments afin de ne pas permettre leur accès depuis l'extérieur. Ainsi, on rend souvent la plupart des champs d'une classe inaccessibles de l'extérieur. Pourquoi ? Tout simplement pour éviter une modification hors de tout contrôle, car rappelons-le, une classe peut constituer une petite usine à gaz qui ne demande qu'à exploser lorsqu'un champ prend une valeur imprévue. Il sera possible, en définissant des méthodes dédiées, de permettre la lecture de tel ou tel champ et même d'autoriser l'écriture de manière contrôlée en passant par des méthodes. Ainsi, pour un champ, qu'on a en général intérêt à rendre innaccesslble de l'extérieur, on programme en général deux méthodes appelées accesseurs, que l'on laisse visibles de l'extérieur. L'un de ces deux accesseurs est en général une fonction (mais qui reste une méthode puisque c'est un membre d'une classe, vous me suivez ?) qui permet de lire la valeur du champ. Le second accesseur est en général une procédure (encore une méthode) qui permet de fixer la valeur du champ, ce qui permet d'effectuer des tests et éventuellement de refuser la nouvelle valeur proposée si elle risque de compromettre le bon fonctionnement de l'objet sur lequel elle a été appelée.

Dès qu'on veut décider de la visibilité des membres vis-à-vis de l'extérieur, il faut préfixer la déclaration de ces membres à l'aide d'un des 4 mots-clé private, protected, public ou published. Il est possible de déclarer la visibilité de chaque membre individuellement mais on préfère généralement regrouper les membres par groupes de visibilité en débutant chaque groupe par le mot réservé qui convient. Chaque mot réservé dénote une visibilité différente, définissant les restrictions d'accès aux membres ayant cette visibilité. La visibilité décide en fait quels éléments ont accès à un membre particulier. Si ce membre est un champ, y accéder signifie pouvoir le lire ou le modifier, si c'est une méthode, on peut l'exécuter (nous verrons plus loin le cas des propriétés). En général, le constructeur et le destructeur ont la visibilité public. Voici la signification des 4 visibilités possibles dans Pascal Objet (en fait, il en existe une cinquième, automated, je n'en dirai pas un mot de plus) :

  • private (membres privés)

    C'est la visibilité la plus faible, qui permet de restreindre au maximum les accès "sauvages" (non autorisés). Les membres ayant cette visibilité ne sont accessibles que dans l'unité où la classe est déclarée (et PAS comme dans certains langages uniquement dans la classe). Tout élément de la même unité a accès à ces membres. Les membres des classes descendantes déclarées dans une autre unité n'ont pas accès à ces membres. Cette visibilité est idéale pour cacher les détails d'implémentation d'une classe (en ne laissant visible au programmeur que ce qui le concerne et qui n'est pas "dangereux" à utiliser (avez-vous remarqué à quel point l'informatique et plus particulièrement la programmation est une activité où reigne la paranoîa ?)).

  • protected (membres protégés)

    Visibilité intermédiaire. Elle donne accès à tous les éléments autorisés par la visibilité private et y ajoute l'ensemble des membres des classes descendantes de la classe du membre, même et surtout celles qui ne sont pas déclarées dans la même unité. Il est par contre impossible d'accéder à ces éléments depuis un autre endroit d'une application, ce qui permet d'accéder à ces membres lorsqu'on crée une classe descendante de leur classe d'appartenance, sans pour autant les laisser en libre accès.

  • public (membres publics)

    Visibilité maximale (au même titre que published). Les membres ayant cette visibilité sont accessibles de partout, c'est-à-dire depuis tous les endroits autorisés par protected, auxquels viennent s'ajouter l'ensemble des éléments des unités utilisant (au sens de uses) l'unité où est déclarée la classe. Lorsqu'aucune visibilité n'est définie pour les membres d'une classe, c'est cette visibilité qui est appliquée par défaut.

  • published (membres publiés)

    Visibilité spéciale, réservée aux propriétés (nous allons y venir, ne vous inquiétez pas, ce n'est pas très compliqué). Les propriétés déclarées dans ce groupe de visibilité ont la particularité d'apparaître dans l'inspecteur d'objets dans la page des propriétés ou des événements (selon les cas) lorsqu'une instance de cette classe est éditée. Certaines restrictions s'appliquent ici, nous aurons tout le loisir d'y revenir en détail.

Nous allons modifier la visibilité des différents membres des classes que nous avons vues jusqu'à présent. Les éléments qui n'ont pas a être modifiés de l'extérieur vont être placés dans une section privée, et les autres dans une section publique. Nous n'utiliserons pas de section protégée car aucune classe descendante des nôtres n'est au programme dans une autre unité. A l'occasion, nous allons écrire des accesseurs, à savoir, je le rappelle, des méthodes dédiées à l'accès en lecture et éventuellement en écriture de certains champs.

Commençons par la classe "AppareilAEcran" : voici sa nouvelle déclaration, qui ne change rien au niveau de l'implémentation.

 
Sélectionnez

 AppareilAEcran = class
  private
    // les 3 éléments ci-dessous ont une visibilité "privée"
    poids: integer;
    longueur_diagonale: integer;
    allume: boolean;
  public
    // les 4 éléments ci-dessous ont une visibilité "publique"
    procedure allumer;
    procedure eteindre;
    constructor Create; virtual;
    destructor Destroy; override;
  end;

Vous pouvez voir dans l'extrait de code précédent comment on déclare une section privée (mot-clé private en général seul sur une ligne, suivi des déclarations des membres dont la visibilité doit être privée) ou publique (de même). Il est à noter que dans chacune de ces sections, les champs doivent être tous déclarés d'abord, et les méthodes ensuite. Plusieurs sections de chaque sorte peuvent être présentes, mais cela a en général peu d'intérêt et il est recommandé d'utiliser le schéma suivant :

 
Sélectionnez

ClasseDescendante = class(ClasseParente)
private
  { déclarations de membres privés }
protected
  { déclarations de membres protégés }
public
  { déclarations de membres publics }
published
  { déclarations de membres publiés }
end;

Le fait de déclarer les champs comme privés pose cependant le souci suivant : le code source qui les manipulait directement auparavant est maintenant caduc, et devra être modifié. Pour accéder aux champs, il va nous falloir pour chaque une méthode de lecture (généralement, on parle de "get", en pur jargon informatique) et éventuellement une autre d'écriture (on parle de "set"). Voici la déclaration de deux méthodes permettant l'accès en lecture et en écriture du champ "poids" d'un appareil à écran :

 
Sélectionnez

 ...
    { dans la classe AppareilAEcran, section publique }
    destructor Destroy; override;
    function getPoids: integer;
    procedure setPoids(valeur: integer);
  end;
...

L'implémentation de la méthode de lecture est des plus simple puisqu'il suffit de retourner en résultat la valeur du champ dont la valeur est demandée. La voici :

 
Sélectionnez

function AppareilAEcran.getPoids: integer;
begin
  Result := poids;
end;

C'est en écrivant le code source de la méthode d'écriture que l'on comprend mieux l'intérêt de ce genre de procédé. Il va être possible de tester la valeur fournie avant d'accepter l'écriture de cette valeur. Les valeurs négatives seront ainsi refusées. Voici le code source de cette méthode :

 
Sélectionnez

procedure AppareilAEcran.setPoids(valeur: integer);
begin
  if valeur >= 0 then
    poids := valeur;
end;

L'avantage d'avoir programmé ces deux méthodes et de les laisser en accès public tandis que le champ qui se cache derrière est en accès privé est que le programmeur, vous ou quelqu'un d'autre, est dans l'incapacité de "tricher" en affectant une valeur de manière sauvage au champ, ce qui pourrait compromettre le bon fonctionnement de l'objet. L'obliger à utiliser les méthodes de lecture et d'écriture sécurise le programme, ce qui est très important car c'est autant de bugs évités sans effort.

Ce sera tout pour ce qui est des visibilités. Nous reviendrons si l'occasion se présente sur la visibilité protégée, et surtout sur la visibilité publiée lorsque nous parlerons de la création de composants (qui sera l'objet d'un autre chapitre, vu que le sujet est très vaste...).

XVI-C-6. Propriétés

Les propriétés sont une des spécificités de Pascal Objet dans le domaine de la programmation orientée objets. A ma connaissance, on ne les retrouve dans aucun autre langage objet, et c'est bien dommage car elles procurrent des fonctionnalités très intéressantes.

Les propriétés en Pascal Objet existent sous un bon nombre de formes et mériteraient un chapitre entier. Pour ne pas vous décourager, je vais plutôt m'attacher ici à décrire d'abord les plus simples, puis à augmenter progressivement la difficulté pour vous montrer tout ce qu'on peut faire à l'aide des propriétés. Nous nous limiterons cependant aux possibilités de base en laissant les plus complexes à un futur chapitre sur la création de composants.

XVI-C-6-a. Propriétés simples

Une propriété est un membre spécial d'une classe permettant de fournir un accès en lecture et éventuellement en écriture à une donnée en ne révélant pas d'où on la sort lors de la lecture ni où elle va lors de l'écriture. Une propriété a un type qui donne le type de la donnée que l'on peut lire et éventuellement écrire. Ce type peut être de n'importe quel type prédéfini de Pascal Objet, ou un type créé dans un bloc type, y compris une classe.

Nous allons continuer l'écriture de nos trois classes en créant une première propriété permettant l'accès au poids. Voici la déclaration de cette propriété, telle qu'elle doit apparaître dans la section publique de la classe "AppareilAEcran" :

 
Sélectionnez

property Poids: integer
  read getPoids
  write setPoids;

Cette déclaration est celle d'une nouevlle propriété dont le nom est "Poids", dont le type est "integer", qu'on lit (read) via la méthode "getPoids" et qu'on écrit (write) via la méthode "setPoids". La propriété "Poids" va nous permettre d'accéder de manière sélective au champ qui porte pour l'instant le même nom (nous corrigerons cela plus tard, c'est une erreur). Lorsqu'on utilisera "Poids" dans un contexte de lecture, la méthode "getPoids" sera appelée de manière transparente et son résultat sera la valeur lue. Lorsqu'on utilisera "Poids" dans un contexte d'écriture (une affectation par exemple), la méthode "setPoids" sera de même appelée de manière transparente et son paramètre sera la valeur qu'on tente d'affecter à la propriété, ce qui fait que l'affectation pourra très bien ne pas être faite, par exemple si la valeur transmise est négative (rappelez-vous que la méthode "setPoids" ne fonctionne qu'avec des valeurs positives). Il est à noter que les méthodes "getPoids" et "setPoids" doivent respecter une syntaxe particulière. "getPoids" doit être une fonction sans paramètre retournant un entier, ce qui est bien le cas, et "setPoids" doit être une procédure à un seul paramètre de type entier, ce qui est également le cas. Ces deux méthodes étant déjà programmées, il n'y a rien de plus à ajouter pour que la propriété fonctionne (mis à par un changement de nom du champs "poids").

Il y a effectivemùent un petit souci puisque nous venons de déclarer deux membres avec le même nom, à savoir le champ "poids" et la propriété "Poids". Dans ce genre de situation, on a pour habitude (une habitude piochée dans le code source des composants de Delphi, ce qui prouve son bon sens) de préfixer les champs par un f ou un F (pour "Field", soit "Champ" en anglais). Ainsi, le champ poids sera renommé en "fPoids" et la propriété "Poids" gardera son nom. Afin de clarifier les choses, voici la déclaration complètement mise à jour de la classe "AppareilAEcran" :

 
Sélectionnez

 AppareilAEcran = class
  private
    fPoids: integer;
    longueur_diagonale: integer;
    allume: boolean;
  public
    procedure allumer;
    procedure eteindre;
    constructor Create; virtual;
    destructor Destroy; override;
    function getPoids: integer;
    procedure setPoids(valeur: integer);
    property Poids: integer
      read getPoids
      write setPoids;
  end;

N'oubliez pas de modifier le code source des deux méthodes "getPoids" et "setPoids" pour faire référence au champ "fPoids" et non à la propriété "Poids", car sinon je vous fais remarquer qu'il y aurait certainement des appels des deux méthodes en boucle lors de la lecture ou de l'écriture de "Poids" puisque chaque lecture appelle "getPoids" et chaque écriture appelle "setPoids".

Le fait d'utiliser une propriété a un autre intérêt majeur quand on part du principe qu'un minimum de visibilité pour chaque membre est une bonne idée (et c'est un bon principe !), est que les méthodes employées pour effectuer la lecture et l'écriture de la valeur de la propriété peuvent être "cachées", c'est-à-dire placées dans la section private de la classe. Ceci oblige à se servir de la propriété sans se servir explicitement des méthodes, ce qui cache au maximum le détail des opérations de lecture et d'écriture de la valeur : le programmeur n'a que faire de ces détails tant qu'ils sont effectués correctement. Déplacez donc les deux déclarations des méthodes "getPoids" et "setPoids" dans la section privée (private) de la déclaration de classe.

Afin de voir les appels implicites des méthodes "getPoids" et "setPoids", nous allons créer une mini-application exemple en modifiant le code des deux méthodes "getPoids" et "setPoids" pour qu'elles signalent leur appel par un simple "ShowMessage". Voici leur nouveau code source temporaire (n'oubliez pas d'ajouter les unité Dialogs (pour ShowMessage) et SysUtils (pour IntToStr) à la clause uses de la partie interface ou implémentation de l'unité) :

 
Sélectionnez

function AppareilAEcran.getPoids: integer;
begin
  ShowMessage('Appel de la méthode getPoids. Valeur retournée : '+IntToStr(fPoids));
  Result := fPoids;
end;

procedure AppareilAEcran.setPoids(valeur: integer);
begin
  if valeur >= 0 then
    begin
      ShowMessage('Appel de la méthode setPoids avec une valeur positive (acceptée) : '+IntToStr(valeur));
      fPoids := valeur;
    end
  else
    ShowMessage('Appel de la méthode setPoids avec une valeur négative (refusée) : '+IntToStr(valeur));
end;

Il est à noter que du fait de l'héritage et de sa visibilité publique, les deux classes descendantes "Television" et "EcranOrdinateur" ont accès à la propriété "Poids", mais pas aux deux méthodes "getPoids" et "setPoids" ni au champ "fPoids", ce qui interdit de surcharger les méthodes ou l'accès direct au champ sans passer par la propriété. Vous noterez que le fait d'utiliser une propriété laisse l'accès au "Poids" tout en limitant sa modification (poids positif).

Créez une nouvelle application et ajoutez-y l'unité dans son état actuel. Placez une zone d'édition (edPoids) et deux boutons (btModifPoids et btQuitter) sur la fiche principale. Faites fonctionner le bouton "Quitter". Le principe de l'application va être tout simplement de proposer l'édition du poids d'une télévision. Le poids sera indiqué dans la zone d'édition, et la propriété "Poids" d'un objet de classe "Television" sera affectée lors du clic sur le bouton "Modifier Poids". Suivra une lecture pour connaître la valeur actuelle du poids, au cas où l'affectation précédente aurait été rejetée. Cette valeur sera placée dans la zone d'édition.

A cette occasion, nous allons apprendre à utiliser une propriété. Une propriété accessible en lecture/écriture s'utilise en tout point comme une variable ayant le type de la propriété, mis à part que la lecture et l'écriture sont effectuées de manière transparente par les accesseurs ("getPoids" et "setPoids" pour le poids). Une propriété peut être en lecture seule (pas de mot-clé write ni de méthode d'écriture dans la déclaration de la propriété) et dans ce cas elle peut être employée comme une constante du type de la propriété.

L'application doit manipuler une télévision : il nous faut donc un objet de classe "Television". Le plus simple, et le mieux pour vous faire manipuler des notions encore toutes chaudes et de rajouter un champ à la classe qui définit la fiche. Ajoutons donc un champ de type (classe) "Television" dans la section privée de la classe en question. Afin de clarifier les choses, voici sa déclaration complète faisant apparaître le champ :

 
Sélectionnez

 TfmPrinc = class(TForm)
    pnPoids: TPanel;
    lbPoids: TLabel;
    edPoids: TEdit;
    btModifPoids: TButton;
    btQuitter: TButton;
  private
    { Déclarations privées }
    fTele1: Television;
  public
    { Déclarations publiques }
  end;

Remarquez dans le code ci-dessus le "f" qui préfixe le nom du champ. Nous avons déclaré un champ de type objet dans une classe, c'est la première fois mais cela n'a rien de très original et ne constitue pas une révolution puisqu'un champ peut être de n'importe quel type autorisé pour une déclaration de variable.

Afin de respecter le « cycle de vie » de l'objet "fTele1", il nous faut le construire puis l'utiliser puis le détruire. La construction peut se faire au moment de la construction de la fiche et la destruction au moment de sa destruction. Notez qu'il serait possible de surcharger le constructeur et le destructeur de la classe pour inclure ces deux instructions, mais Delphi fournit un moyen plus simple que nous avons employé jusqu'à présent et qui épargne beaucoup de soucis : les événements. Les deux qui nous intéressent sont les événements OnCreate et OnDestroy de la fiche. OnCreate se produit pendant la création de la fiche, tandis que OnDestroy se produit lors de sa destruction. Ce sont deux endroits idéaux pour construire/détruire des champs de type objet. Générez les deux procédures de réponse à ces événements et complètez le code en construisant fTele1 dans OnCreate et en le détruisant dans OnDestroy. Voici ce qu'il faut écrire :

 
Sélectionnez

procedure TfmPrinc.FormCreate(Sender: TObject);
begin
  fTele1 := Television.Create;
end;

procedure TfmPrinc.FormDestroy(Sender: TObject);
begin
  fTele1.Destroy;
end;

C'est le moment idéal pour vous parler d'une petite astuce sur laquelle insiste lourdement l'aide de Delphi : la méthode Free. Lorsque vous appelez Destroy sur un objet qui n'a pas été construit, il se produit une erreur fâcheuse qui ne devrait pas effrayer le programmeur consciencieux (et habitué !) car il vérifie toujours ce genre de chose. Bref, ce genre d'erreur n'est pas agréable et il existe un moyen simple de s'en protéger : appeler, à la place de Destroy, la méthode Free. Cette méthode effectue un test de validité sur l'objet avant de le détruire. Voici la modification a effectuer :

 
Sélectionnez

procedure TfmPrinc.FormDestroy(Sender: TObject);
begin
  fTele1.Free;
end;

Nous allons maintenant faire fonctionner l'essentiel de l'application exemple : le bouton "Modifier Poids". Le principe de son fonctionnement a été détaillé plus haut, voici donc le code source qui illustre comment on utilise une propriété tant en lecture qu'en écriture.

 
Sélectionnez

procedure TfmPrinc.btModifPoidsClick(Sender: TObject);
var
  v, e: integer;
begin
  val(edPoids.Text, v, e);
  if e = 0 then
    begin
      fTele1.Poids := v;
      if fTele1.Poids <> v then
        // valeur refusée
        edPoids.Text := IntToStr(fTele1.Poids);
    end
  else
    edPoids.Text := IntToStr(fTele1.Poids);
end;

Comme vous pouvez le voir, un essai de traduction du contenu de la zone d'édition est réalisé grâce à la procédure "val". Ensuite, suivant le résultat de la conversion, la propriété est modifiée et sa valeur après tentative de modification est testée ou la valeur actuelle est replacée dans la zone d'édition. Voici une capture d'écran montrant l'exécution de l'application, dont vous pouvez obtenir le code source ici :

Image non disponible

Ce sera tout pour cete petite application ; vous pourrez l'améliorer à souhait lorsque vous aurez complété l'exercice suivant :

Exercice 1 : (voir la solution).

Transformez la classe "AppareilAEcran" pour que la longueur de diagonale et l'état allumé/éteint soient des propriétés. En ce qui concerne l'état éteint/allumé, l'accesseur en écriture fera appel à eteindre et à allumer qui deviendront des méthodes privées. Vous ferez particulièrement attention à l'endroit où vous déclarez et placez les propriétés et les méthodes dans la déclaration de la classe "AppareilAEcran".

Continuons sur les propriétés. Nous venons de voir qu'il est possible de déléguer la lecture et l'écriture de la valeur d'une propriété chacune à une méthode. Cette manière de faire, qui est courante, a cependant un inconvénient : le fait de devoir écrire parfois une méthode de lecture ou d'écriture rien moins que triviale, à savoir l'affectation de la valeur d'un champ à Result dans le cas de la lecture et l'affectation d'un champ dans le cas d'une écriture. Il est possible, pour ces cas particuliers où l'une ou l'autre des méthodes (voire les deux, dans certains cas très précis) n'est pas très originale, de s'en passer purement et simplement.

Le principe est de dire qu'au lieu d'appeler une méthode pour réaliser une des deux opérations de lecture ou d'écriture, on fait directement cette opération sur un champ qu'on spécifie en lieu et place du nom de méthode. On utilise la plupart du temps cette technique pour permettre la lecture directe de la valeur d'un champ tandis que son écriture est « filtrée » pa une méthode d'écriture. Voici un petit exemple reprenant la classe "TAppareilAEcran", où la propriété "Poids" a été modifiée pour ne plus utiliser de méthode de lecture mais le champ "fPoids" à la place (uniquement en lecture, puisque l'écriture passe toujours par la méthode "setPoids") :

 
Sélectionnez

 AppareilAEcran = class
  private
    fPoids: integer;
    fLongueur_Diagonale: integer;
    fAllume: boolean;
    //function getPoids: integer;
    procedure setPoids(valeur: integer);
    ...
  public
    ...
    destructor Destroy; override;
    property Poids: integer
      read fPoids
      write setPoids;
    ...
  end;

Si vous modifiez l'unité obtenue à l'issue de l'exercice 1, vous constaterez que la modification ne gène pas le compilateur, à condition toutefois de supprimer ou de mettre en commentaire le code source de la méthode "getPoids". En effet, n'étant plus déclarée, cette méthode ne doit plus non plus être implémentée.

Il est également possible de créer une propriété en lecture seule ou en écriture seule (quoique ce cas-là soit d'un intérêt plus que douteux). Nous allons uniquement parler des propriétés en lecture seule. Certaines valeurs, à ne donner qu'à titre de renseignement, ne doivent pas pouvoir être modifiées. Ainsi, une propriété en lecture seule est le meilleur moyen de permettre la lecture tout en rendant l'écriture impossible.

Pour créer une propriété en lecture seule, il suffit de ne pas indiquer le mot clé write ainsi que la méthode ou le champ utilisé pour l'écriture. En ce qui concerne la lecture, vous pouvez aussi bien spécifier un champ qu'une méthode de lecture. Voici la déclaration de deux propriétés en lecture seule permettant d'obtenir, pour un objet de classe "EcranOrdinateur", la résolution horizontale ou verticale actuelle. Comme la résolution ne peut être changée qu'en modifiant à la fois les deux dimensions, on peut lire ces deux dimensions individuellement mais pas les modifier.

 
Sélectionnez

 EcranOrdinateur = class(AppareilAEcran)
  private
    resolutions_possibles: array of TResolution;
    resolution_actuelle: TResolution;
    function changer_resolution(NouvRes: TResolution): boolean;
    function getResolutionHorizontale: word;
    function getResolutionVerticale: word;
  public
    property ResolutionHorizontale: word
      read getResolutionHorizontale;
    property ResolutionVerticale: word
      read getResolutionVerticale;
  end;

Comme vous le voyez, il n'y a rien de très transcendant là-dedans. Les propriétés en lecture seule sont assez peu courantes, mais se révèlent fort pratiques à l'occasion pour remplacer avantageusement une fonction de lecture. L'avantage tient en deux mots : l'inspecteur d'objets. Ce dernier sait, depuis la version 5 de Delphi, afficher les propriétés en lecture seule. Comme leur nom l'indique, il ne permet évidemment pas de modifier leur valeur.

XVI-C-6-b. Propriétés tableaux

Pour l'instant, nous n'avons considéré que des propriétés de type simple : des entiers, des chaînes de caractères, et pourquoi pas, même, des tableaux. Rien n'empêche en effet à une propriété d'être de type tableau : l'élément lu et écrit lors d'un accès à la propriété est alors un tableau complet. Rien ne permettait jusqu'à présent de ne manipuler plus d'un élément par propriété, et c'est là qu'un nouveau concept va nous permettre de nous sortir de cette impasse : les propriétés tableaux.

Ces propriétés tableaux, qui tirent leur nom non pas du fait qu'elles ont quelque chose à voir avec les tableaux, mais du fait de la manière de les utiliser (nous verrons cela plus loin) permettent de manipuler un nombre à priori quelconque de couples « clé-valeur ». Si vous ignorez ce que signifie ce terme, prenons deux petits exemples qui devraient suffire à vous faire comprendre de quoi je parle.

Premier exemple : imaginons un tableau de 10 cases indicées de 1 à 10, contenant des chaînes de caractères. Dans ce cas, ce tableau possède 10 couples clé-valeur où les clés sont les indices de 1 à 10, et les valeurs les chaînes de caractères stockées dans le tableau. La valeur de la clé i est la chaîne stockée dans la case n°i.

Second exemple plus subtil : prenons un enregistrement (record) "TPersonne" comprenant le nom, le prénom et l'âge d'une personne. Cet enregistrement comporte alors 3 couples clé-valeur où les 3 clés sont le nom, le prénom et l'âge. Les trois valeurs correspondantes sont les valeurs associées à ces trois clés. Vous saisissez le principe ?

Delphi permet, via les propriétés tableau, de gèrer une liste de couples clés-valeurs où les clés sont toutes d'un même type et où les valeurs sont également toutes d'un même type (ce qui peut être contourné et l'est fréquemment par l'utilisation d'objets, mais nous y reviendrons lors du paragraphe sur le polymorphisme). Le cas le plus fréquent est d'utiliser des clés sous forme de nombres entiers. Dans un instant, nous allons déclarer une propriété tableau permettant d'accèder aux chaînes mémorisées dans un objet de classe "Television". Mais voici tout d'abord le format de déclaration d'une propriété tableau

 
Sélectionnez

property nom_de_propriete[declaration_de_cle]: type_des_valeurs
  read methode_de_lecture
  write methode_d_ecriture;

Vous noterez ici que ce qui suit read et write ne peut PAS être un champ, contrairement aux propriétés classiques. Vous noterez en outre que la partie write est facultative.

La partie nom_de_propriete est le nom que vous voulez donner à la propriété. C'est, comme pour une propriété non tableau un identificateur. La partie type_des_valeurs donne le type d'une valeur individuelle, à savoir le type de la valeur dans chaque couple clé-valeur. Ainsi, si vous utilisez des couples indexant des chaînes de caractères par des entiers, c'est le type string qui devra être utilisé. Mais la partie intéressante est certainement celle qui figure entre les crochets (qui doivent être présents dans le code source) : elle déclare le type de la clé utilisée dans les couples clé-valeur. Contrairement à la valeur qui doit être un type unique, la clé peut comporter plusieurs éléments, ce qui permet par exemple d'indexer des couleurs par deux coordonnées X et Y entières afin d'obtenir la couleur un pixel aux coordonnées (X, Y) d'une image. Le plus souvent, on n'utilise qu'une seule valeur, souvent de type entier, mais rien n'empêche d'utiliser une clé farfelue définie par un triplet (trois éléments), par exemple un entier, une chaîne et un objet ! La déclaration d'une clé ressemble à la déclaration des paramètres d'une fonction/procédure :

nom_de_parametre1: type1[; nom_de_parametre2: type2][; ...]

(notez ici les crochets italiques qui dénotent des parties facultatives : ces crochets n'ont pas à être placés dans le code source)

Venons-en à un exemple : déclarons une propriété tableau permettant l'accès aux chaînes mémorisées par une télévision. La clé est dans notre cas un entier, et la valeur est également, ce qui est un cas particulier, un entier. Voici la déclaration de la propriété. Nous reviendrons plus loin sur les méthodes de lecture et d'écriture nécessaires :

 
Sélectionnez

 Television = class(AppareilAEcran)
  private
    ...
  public
    constructor Create; override;
    property Chaines[numero: integer]: integer
      read GetChaines
      write SetChaines;
  end;

La seule nouveauté par rapport aux propriétés classiques est la présence d'une déclaration supplémentaire entre crochets, qui fait de cette propriété une propriété tableau. Comme vous le voyez, la propriété s'appuie sur deux accesseurs qui sont obligatoirement des méthodes. Les types de paramètres et l'éventuel type de résultat de ces deux méthodes est imposé par la déclaration de la propriété. Ainsi, la méthode de lecture doit être une fonction qui admet les paramètres définissant la clé, et retournant un résultat du type de la valeur de la propriété. La méthode d'écriture doit être une procédure dont les arguments doivent être ceux de la clé, auquel on ajoute un paramètre du type de la valeur. Ceci permet, lors de la lecture, à la propriété d'appeler la méthode de lecture avec la clé fournie et de retourner la valeur retournée par cette méthode, et à l'écriture, de fournir en paramètre la clé ainsi que la valeur associée à cette clé.

Voici les déclarations des deux accesseurs de la propriété "Chaines" : Voici les déclarations des deux accesseurs de la propriété "Chaines" :

 
Sélectionnez

  ...
    procedure memoriser_chaine (numero, valeur: integer);
    function GetChaines(numero: integer): integer;
    procedure SetChaines(numero: integer; valeur: integer);
  public
    ...

Comme vous pouvez le voir, la méthode de lecture "getChaines" prend les paramètres de la clé, à savoir un entier, et retourne le type de la propriété, à savoir également un entier. La méthode d'écriture "setChaines", quant à elle, prend deux arguments : le premier est celui défini par la clé, et le second est le type de la propriété.

Après la déclaration, passons à l'implémentation de ces méthodes. On rentre ici dans un domaine plus classique où le fait d'être en train d'écrire une méthode de lecture ou d'écriture ne doit cependant pas être pris à la légère. Ainsi, la méthode se doit de réagir dans toutes les situations, c'est-à-dire quelle que soit la clé et la valeur (dans le cas de la méthode d'écriture), même si elles sont incorrectes. La méthode de lecture doit être implémentée de telle sorte qu'en cas de clé valide (c'est à vous de décider quelle clé est valide et quelle autre ne l'est pas) la fonction retourne la valeur qui lui correspond. De même, la méthode d'écriture, si elle existe (si la propriété n'est pas en lecture seule) doit, si la clé et la valeur sont correctes, effectuer les modifications qui s'imposent pour mémoriser le couple en question. Il appartient aux deux méthodes d'effectuer toutes les allocations éventuelles de mémoire lorsque c'est nécessaire : on voit ici l'immense avantage des propriétés qui permettent de lire et d'écrire des valeurs sans se soucier de la manière dont elles sont stockées et représentées.

Voici le code source des deux méthodes "getChaines" et "setChaines" :

 
Sélectionnez

function Television.GetChaines(numero: integer): integer;
begin
  if (numero >= 1) and (numero <= MAX_CHAINES) then
    Result := chaines_memorisees[numero]
  else
    // c'est une erreur, on peut choisir -1 comme valeur retournée en cas d'erreur
    Result := -1;
end;

procedure Television.SetChaines(numero: integer; valeur: integer);
begin
  if (numero >= 1) and (numero <= MAX_CHAINES) then
    if valeur >= 0 then
      chaines_memorisees[numero] := valeur;
  // sinon rien du tout !
end;

Vous pouvez constater qu'on n'autorise plus les valeurs de réglages négatives. C'est un choix arbitraire que nous pouvons faire facilement dans le cas présent mais qui devrait donner lieu à plus de réflexion dans un cas réel. Le fait d'interdire les valeurs négatives permet de distinguer la valeur -1 des autres valeurs, en lui donnant la signification d'erreur. Réalisons maintenant un petit exemple qui nous permettra d'utiliser cette propriété tableau.

Partez de l'application exemple développée pour tester la propriété "Poids" (téléchargez-là ici si vous ne l'avez pas sous la main. Attention : cette version inclue les messages indiquant des accès en lecture et en écriture pour la propriété Poids). Améliorez l'interface en la faisant ressembler à ceci :

Image non disponible

Le principe de la partie ajoutée sera le suivant : lors d'un clic sur le bouton "Lire", la valeur de la clé dont le numéro est indiqué dans la zone d'édition "clé" est lue, et écrite dans la zone d'édition "valeur". En cas d'erreur (lecture d'une valeur pour une clé qui n'existe pas, un message d'erreur est affiché. Lorsqu'un clic sera effectué sur le bouton "Ecrire", la valeur dans la zone d'édition "valeur" est affectée à la propriété "Chaines" avec la clé indiquée dans la zone d'édition "clé". En cas d'erreur de clé, un message d'erreur est affiché ; en cas d'erreur de valeur, l'ancienne valeur est rétablie dans la zone d'édition.

Une propriété tableau s'utilise comme une propriété, à savoir qu'on peut l'utiliser comme une constante lorsqu'elle est en lecture seule et comme une variable lorsqu'elle est en lecture/écriture. Une propriété tableau doit cependant être utilisée en spécifiant la clé à laquelle on désire accéder. Ainsi, la propriété doit être suivie d'une paire de crochets à l'intérieur desquels on place la valeur de la clé. Ainsi, par exemple, « Chaines[1] » fera référence à la valeur associée à la clé 1. Comme beaucoup de propriétés sont indexées (la clé est parfois appelée l'index de la propriété) par un simple entier partant de 0, l'utilisation d'une propriété tableau ressemble souvent à l'utilisation d'un simple tableau avec cependant la restriction des indiecs en moins puisque tous les indices sont utilisables (même s'il n'y a pas de valeur correspondante).

Dans notre cas, si on considère un objet "Tele1" de classe "Television", "Tele1.Chaines[1]" fait référence à la chaîne dont le numéro est 1. Le type de cette expression est celui de la valeur de la propriété tableau, à savoir un entier. Comme la propriété est en lecture/écriture, cette expression peut être utilisée comme une variable, à l'exception près que lors d'une lecture, la valeur -1 signale que la valeur n'existe pas (clé incorrecte). Voici le code source de la méthode associée au clic sur le bouton "Lire" :

 
Sélectionnez

procedure TfmPrinc.btLireClick(Sender: TObject);
var
  v, e, valeur: integer;
begin
  val(edCle.Text, v, e);
  if e = 0 then
    begin
      valeur := fTele1.Chaines[v];
      if valeur <> -1 then
        edValeur.Text := IntToStr(valeur)
      else
        ShowMessage('La clé ' + IntToStr(v) + ' n''existe pas.');
    end
  else
    ShowMessage('La clé doit être une valeur entière.');
end;

Remarquez l'endroit surligné où l'on accède à la propriété "Chaines". Vous voyez qu'on utilise la prorpiété comme un tableau (c'est pour cela qu'on appelle ces propriétés des propriétés tableaux). Voici, pour enfoncer le clou et vous montrer que les propriétés tableaux sont simples à utiliser, le code source de la méthode réalisant l'écriture :

 
Sélectionnez

procedure TfmPrinc.btEcrireClick(Sender: TObject);
var
  cle, e1, valeur, e2: integer;
begin
  val(edCle.Text, cle, e1);
  val(edValeur.Text, valeur, e2);
  if (e1 = 0) and (e2 = 0) then
    begin
      // écriture
      fTele1.Chaines[cle] := valeur;
      // vérification
      valeur := fTele1.Chaines[cle];
      if (valeur <> -1) then
        edValeur.Text := IntToStr(valeur)
      else
        ShowMessage('La clé ' + IntToStr(cle) + ' n''existe pas.');
    end
  else if (e2 <> 0) then
    ShowMessage('La valeur doit être une valeur entière.')
  else // e1 <> 0
    ShowMessage('La clé doit être une valeur entière.');
end;

Les lignes surlignées sont les seules qui nous intéressent ici. La première effectue, et c'est nouveau, une écriture de la propriété Chaines avec une clé donnée justement par la variable "cle". La clé et valeur sont obtenues par conversion des contenus des deux zones d'édition correspondantes, puis il y a tentative d'écriture de la propriété. Afin de vérifier si l'indice utilisé était correct, on effectue une lecture. Dans le cas où cette lecture retourne un résultat valide, on l'affiche dans la zone d'édition (ce qui en fait, si on y réfléchit, ne sert à rien d'autre qu'à vous donner un exemple). En cas d'invalidité, un message d'erreur est affiché.

Ce sera tout pour cet exemple. Nous venons de voir comment utiliser une propriété tableau simple, puisque les types de la clé et de la valeur de la propriété sont des entiers. Nous allons maintenant voir une notion sur les propriétés puis revenir aux exemples sur les propriétés tableau car la notion que nous allons voir est directement liée aux propriétés tableau.

XVI-C-6-c. Propriété tableau par défaut

Lorsqu'un objet est créé majoritairement pour faire fonctionner une propriété tableau (nous verrons un exemple tout de suite après l'explication), c'est-à-dire quand la propriété tableau est incontesttablement le membre le plus important d'une classe, il peut être intéressant de simplifier son utilisation en en faisant une propriété tableau par défaut. Ce terme signifie que la manière de faire référence à cette propriété va être simplifiée dans le code source, cela d'une manière que nous allons voir dans un instant. Une seule propriété tableau peut être définie par défaut, et on peut alors y avoir accès sans mentionner le nom de la propriété : l'objet est simplement suffixé par la clé de la propriété tableau par défaut, et c'est à cette propriété qu'on s'adresse. Voici ce qu'il faut écrire sans propriété tableau par défaut :

 
Sélectionnez

fTele1.Chaines[cle] := valeur;

et avec :

 
Sélectionnez

fTele1[cle] := valeur;

Reconnaissez que c'est mieux, et pas très compliqué. Si vous croyez que c'est compliqué, pensez à une ligne de code du style :

 
Sélectionnez

ListBox1.Items[1] := 'test';

Dans cette ligne de code, Items est un objet de classe TStrings, qui comporte une propriété par défaut, que vous utilisez puisque vous y faites référence avec la clé "1". Son nom, vous ne le connaissez pas, c'est "Strings". Votre ignorance de ce nom ne vous a pas empêché d'utiliser la propriété en question de manière implicite. Reste à savoir comment faire d'une propriété tableau une propriété tableau par défaut. C'est on ne peut plus simple et ça tient en un mot. Voici :

 
Sélectionnez

 ...
    constructor Create; override;
    property Chaines[numero: integer]: integer
      read GetChaines
      write SetChaines; default;
  end;
  ...

Le mot-clé default précédé et suivi d'un point-virgule (c'est important, car lorsqu'il n'est pas précédé d'un point-virgule, il a une autre signification) signale qu'une propriété tableau devient la propriété tableau par défaut. Il est bien évident que la propriété que vous déclarez par défaut doit être une propriété tableau, vous vous attirerez les foudres du compilateur dans le cas contraire. Mais voyons plutôt ce que cela change par exemple dans la méthode d'écriture de notre petite application exemple. Voyez les endroits surlignés pour les changements :

 
Sélectionnez

procedure TfmPrinc.btEcrireClick(Sender: TObject);
var
  cle, e1, valeur, e2: integer;
begin
  val(edCle.Text, cle, e1);
  val(edValeur.Text, valeur, e2);
  if (e1 = 0) and (e2 = 0) then
    begin
      // écriture
      fTele1[cle] := valeur;
      // vérification
      valeur := fTele1[cle];
      if (valeur <> -1) then
        edValeur.Text := IntToStr(valeur)
      else
        ShowMessage('La clé ' + IntToStr(cle) + ' n''existe pas.');
    end
  else if (e2 <> 0) then
    ShowMessage('La valeur doit être une valeur entière.')
  else // e1 <> 0
    ShowMessage('La clé doit être une valeur entière.');
end;

Comme vous pouvez le constater, l'utilisation judicieuse de cette fonctionnalité n'est pas très coûteuse en temps de programmation (c'est le moins qu'on puisse dire), mais surtout en fait ensuite gagner non seulement en concision mais également en clarté si la propriété est bien choisie. Pour l'exemple ci-dessus, c'est discutable, mais pour ce qui est de l'exemple ci-dessous, c'est indiscutable.

Supposons un instant que nous ne connaissions pas les propriétés tableau par défaut. Le fait de modifier l'élément n°3 d'une liste défilante (ListBox) s'écrirait ainsi :

 
Sélectionnez

ListBox1.Items.Strings[2] := 'test';

C'est, comment dire... un petit peu lourd. Le fait que "Strings" soit une propriété par défaut est très justifié puisque l'objet "Items" (qui est en fait lui-même une propriété de la classe "TListBox" de classe "TStrings") ne sert qu'à manipuler des chaînes de caractères. AInsi, on met en avant, en faisant de "Strings" une propriété tableau par défaut, l'accès en lecture ou en écriture à ces chaînes de caractères, tout en laissant le reste utilisable classiquement. Puisque "Strings" est par défaut, voici évidemment ce qu'on utilisera systématiquement (il ne faudra surtout pas s'en priver puisque les deux écritures sont équivalentes) :

 
Sélectionnez

ListBox1.Items[2] := 'test';

Vous en savez maintenant assez sur les propriétés et sur la programmation objet sous Delphi pour être capable de mener à bien un mini-projet. Dans ce mini-projet, vous aurez à manipuler nombre de connaissances apprises au cours des paragraphes précédents.

XVI-C-7. Mini-projet n°5 : Tableaux associatifs

Certains langages de programmation comme PHP proposent un type de données particulièrement intéressant qui n'a aucun équivalent en Pascal Objet. Ce genre de tableau utilise en fait n'importe quel indice pour contenir n'importe quelle donnée. Ainsi, on peut trouver dans un tableau associatif une chaîne indexée par un entier et un enregistrement indexé par une chaîne. La taille de ce type de tableau est en outre variable, et dépend des éléments ajoutés et retirés du tableau associatif.

Ce mini-projet consiste à réaliser une classe permettant l'utilisation d'un tableau associatif simple indexé uniquement par des chaînes de caractères et dont chaque élément est également une chaîne de caractères. Pour ce qui est du stockage d'un nombre quelconque d'éléments, vous avez le droit d'utiliser la classe "TList" qui permet de sauvegarder un nombre variable de pointeurs. L'accès aux éléments du tableau associatif doit se faire via une propriété tableau par défaut, pour un maximum de simplicité. Ainsi, les instructions suivantes, où "TA" est un tableau associatif, se doivent de fonctionner :

 
Sélectionnez

TA['prénom'] := 'Jacques';
TA['nom'] := 'Dupont';
TA['age'] := '53';
ShowMessage(TA['prénom'] + ' ' + TA['nom'] + ' a ' + TA['age'] + ' ans.');

Vous pouvez vous lancer seul sans aide mais je vous recommande de suivre les étapes de progression, qui vous donnent également les clés pour vous servir des éléments que vous ne connaissez pas encore (comme l'utilisation de la classe "TList"). Vous pourrez également y télécharger le mini-proje résolu.

XVI-D. Notions avancées de programmation objet

La plupart des gens se contenteront sans sourciller des connaissances abordées dans la partie précédentes. Sachez que c'est largement insuffisant pour prétendre connaître et encore moins maîtriser la programmation objet. Dans cette partie, nous allons aborder quelques notions plus ardues comme l'héritage et le polymorphisme. Nous (re)parlerons également de notions très pratiques et plus accessibles comme la surcharge (plusieurs notions sont à mettre sous cette appelation générale), la redéfinition et les méthodes de classe. Essayez, même si vous aurez peut-être un peu plus de mal que pour la partie précédente, de suivre cette partie car elle contient les clés d'une bien meilleure compréhension de la programmation objet. Il est à noter que les interfaces et les références de classe ne sont pas au programme.

XVI-D-1. Retour sur l'héritage

Cela fait déjà un certain temps que je vous nargue avec ce terme d'héritage sans lui donner plus qu'une brêve définition. Le moment est venu d'aborder la chose.

Comme vous l'avez déjà vu, la programmation objet se base sur la réutilisation intelligente du code au travers des méthodes. A partir du moment où un membre est déclaré dans une classe, il est possible (sous certaines conditions) de le réutiliser dans les classes dérivées. Les conditions sont que le membre soit directement accessible (il doit donc être au plus "protégé" si on y accède depuis une autre unité) soit accessible via une méthode qui l'utilise et à laquelle on a accès. Le mécanisme qui vous permet cet accès, l'héritage, est intimement lié à la notion de polymorphisme que nous verrons plus loin. L'héritage permet d'augmenter les possibilité au fur et à mesure que l'on crée une hiérarchie de classes.

La hiérarchie en question, où une classe est une branche d'une classe noeud parent si elle est une sous-classe directe de celle-ci (si elle l'étend, donc), est justement appelé hiérarchie d'héritage des classes. Nous avons vu de petits extraits de telless hiérarchies au chapitre 10 mais les connaissances en programmation objet que je pouvais supposer acquises à l'époque ne me permettaient pas de trop m'étendre sur le sujet. L'héritage permet donc, à condition d'avoir accès à un membre, de l'utiliser comme s'il était déclaré dans la même classe. L'héritage rend possible la constitution étape par étape (en terme d'héritage) d'une vaste gamme d'objets aux comportements personnalisés mais possèdant cependant pour une grande part un tronc commun. Par exemple, tous les composants ont une base commune : la classe TComponent. Cette classe regroupe tous les comportements et les fonctionnalités minimums pour un composant. Il est possible de créer des composants ayant d'un seul coup toutes les fonctionnalités de TComponent grâce au mécanisme d'héritage.

L'héritage permet également une autre fonctionnalité qui pour l'instant est plutôt restée dans l'ombre : la possibilité d'avoir, au fur et à mesure qu'on avance dans une branche d'héritage (plusieurs classes descendantes les unes des autres de manière linéaire), une succession de méthodes portant le même nom mais effectuant des actions différentes. Selon la classe dans laquelle on se trouve, la méthode ne fait pas la même chose (en général, elle en fait de plus en plus au coours des héritages). C'est ici une autre notion qui entre en jeu : le polymorphisme.

XVI-D-2. Polymorphisme

Le polymorphisme est aux objets ce que le transtypage est aux autres types de données. C'est une manière un peu minimaliste de voir les choses, mais c'est une bonne entrée en matière. Si le polymorphisme est plus complexe que le transtypage, c'est que nos amies les classes fonctionnent sur le principe de l'héritage, et donc qu'en changeant la classe d'un objet, on peut peut-être utiliser des méthodes et des propriétés existant dans la classe d'origine, mais ayant un comportement différent dans la classe d'arrivée. Le polymorphisme est en fait le mécanisme qui permet à un objet de se comporter temporairement comme un objet d'une autre classe que sa classe d'origine.

Attention, ici, par classe d'origine, on ne veut pas dire la classe qui a servi à déclarer l'objet mais celle qui a servi à le construire. Jusqu'ici, les deux classes ont toujours été les mêmes, mais il est bon de savoir qu'elles peuvent tout à fait être différentes. En fait, on peut déclarer un objet d'une classe et le construire à partir d'une autre. Dans l'absolu, il faut se limiter à la classe servant à déclarer l'objet et ses sous-classes, faute de quoi les résultats sont imprévisibles. Prenons de suite un exemple de code. N'essayez pas de le compiler dans l'état actuel :

 
Sélectionnez

procedure TfmPrinc.Button1Click(Sender: TObject);
var
  Tele: AppareilAEcran;
begin
  Tele := Television.Create;
  Tele.Chaines[1] := 1;
  Tele.Destroy;
end;

On a déclaré "Tele" comme étant un objet de classe "AppareilAEcran". En fait, "Tele" peut maintenant être soit un objet de classe "AppareilAEcran", soit être un objet polymorphe d'une classe descendante de "AppareilAEcran". La preuve en est faite par la première instruction qui crée un objet de classe "Television" et l'affecte à "Tele". "Tele" est donc déclaré comme étant un objet de classe "AppareilAEcran et se comportera comme tel en apparence, mais c'est bel et bien un objet de classe "Television". Si vous tentez de compiler le code source présent ci-dessus, le compilateur indique une erreur sur la seconde ligne. En effet, la classe "AppareilAEcran" ne comporte pas de propriété "Chaines". Cette propriété existe pourtant bien pour l'objet "Tele", mais du fait de la déclaration en "AppareilAEcran", la propriété est invisible. Vous venez de voir un premier aspect du polymorphisme.

Le polymorphisme permet en fait, à partir d'un objet déclaré d'une classe, d'y placer un objet de n'importe quelle classe descendante. Ce genre de comportement est très utile par exemple pour donner un objet en paramètre, lorsqu'on sait que l'objet pourra être d'une classe ou de ses sous-classes. C'est le cas d'un paramètre que vous avez rencontré si souvent que vous ne le voyez plus : le voici :

 
Sélectionnez

Sender: TObject

Ca vous dit quelque chose ? C'est tout simplement le paramètre donné à la quasi-totalité des méthodes de réponse aux événements. Ce paramètre qui ne nous a jamais encore servi est déclaré de classe TObject, mais du fait du polymorphisme, peut être de n'importe quelle classe descendante de TObject, c'est-à-dire, si vous avez bien appris votre leçon, que ce paramètre peut être de n'importe quelle classe. Nous le voyons (dans la méthode à laquelle il est passé) d'une manière réduite puisque pour nous c'est un objet de classe "TObject", mais il faut savoir qu'en fait le "Sender" est rarement un objet de cette classe. Vrai ? Passons à un exemple :

 
Sélectionnez

procedure TfmPrinc.Button2Click(Sender: TObject);
var
  O, Test: TObject;
begin
  O := TObject.Create;
  Test := TList.Create;
  ShowMessage('0 est de classe ' + O.ClassName);
  ShowMessage('Sender est de classe ' + Sender.ClassName);
  ShowMessage('Test est de classe ' + Test.ClassName);
  Test.Destroy;
  O.Destroy;
end;

le membre "ClassName" employé ci-dessus est une méthode de classe : nous y reviendrons, ce n'est pas là le plus important. Elle retourne le nom de la classe véritable de l'objet pour lequel on l'appelle. Ce qui est important, c'est le résultat de l'exécution de cette petite méthode :

Image non disponible

Comme vous pouvez le constater, "ClassName" ne retourne pas les résultats auquel on pourrait s'attendre en n'y regardant pas de plus près et en ne connaissant pas le polymorphisme. "O" s'avère être, comme nous pouvions nous y attendre, un objet de classe "TObject". "Sender" est, lui, un objet de classe "TButton". Pour répondre à une question que vous vous posez sans doute, c'est l'objet correspondant au bouton sur lequel vous avez cliqué. Il est transmis sous la forme d'un objet de classe TObject mais c'est en fait un objet de classe "TButton". Enfin, "Test" est, comme sa construction le laisse présager, un objet de classe "TList". Bilan : trois objet apparamment de classe "TObject", dont l'un en est vraiment un, et dont deux autres qui sont en quelque sorte des usurpateurs.

Je pense que ces quelques exemples vous ont permi de cerner le premier principe du polymorphisme : sous l'apparence d'un objet d'une classe peut se cacher un objet d'une de ses classes descendantes.

Lorsqu'on a affaire à un objet polymorphe, on peut avoir besoin de lui redonner temporairement sa classe d'origine. Ainsi, il existe deux mots réservés de Pascal Objet qui vont nous permettre deux opérations basiques : tester si un objet est d'une classe donnée, et changer temporairement la classe d'un objet par polymorphisme. Ainsi, l'opérateur is permet de tester un objet pous savoir s'il est d'une classe donnée ou d'une de ses classes ancètres (donc en remontant l'arbre d'héritage et non plus en le descendant). Il s'utilise ainsi, en tant qu'expression booléenne (vraie ou fausse) :

Objet is Classe

Voici de suite un exemple démontrant son utilisation :

 
Sélectionnez

procedure TfmPrinc.Button2Click(Sender: TObject);
begin
  if Sender is TObject then
    ShowMessage('Sender est de classe TObject');
  if Sender is TButton then
    ShowMessage('Sender est de classe TButton');
  if Sender is TEdit then
    ShowMessage('Sender est de classe TEdit');
end;

Si vous exécutez cette méthode, vous verrez que "Sender" est considéré par is comme un objet de classe "TObject" et de classe "TButton", mais pas de classe "TEdit". En fait, puisque "Sender" est un objet dont la classe véritable est "TButton", il est valable de dire qu'il est de classe "TObject" ou de classe "TButton", mais il n'est certainement pas de classe "TEdit" puisque "TButton" n'est pas une classe descendante de "TEdit".

L'opérateur is est en fait pratique pour s'assurer qu'on peut effectuer un changement de classe (pas exactement un transtypage puisqu'on parle de polymorphisme) sur un objet. Ce changement de classe peut s'effectuer via l'opérateur as. Ainsi, l'expression :

Objet as Classe

peut être considéré comme un objet de classe "Classe". Si la classe n'est pas une classe ancêtre de la classe d'origine de l'objet, une exception se produit. Voici une petite démonstration de l'utilisation de as conjointement à is :

 
Sélectionnez

procedure TfmPrinc.Button2Click(Sender: TObject);
begin
  if Sender is TButton then
    (Sender as TButton).Caption := 'Cliqué !';
end;

La méthode ci-dessus teste d'abord la classe de "Sender". Si "TButton" est une classe ancêtre (ou la bonne classe) pour "Sender", il y a exécution de l'instruction de la deuxième ligne. Cette instruction se découpe ainsi :

  1. "Sender as TButton" se comporte comme un objet de classe "TButton", ce qui va permettre l'accès à ses propriétés ;
  2. "(Sender as TButton).Caption" s'adresse à la propriété "Caption" du bouton.
  3. Enfin, l'instruction complète affecte une nouvelle valeur à la propriété.

Lorsque vous exécutez le code en question, en cliquant sur le bouton, vous aurez la surprise (?) de voir le texte du bouton changer lors de ce clic. Remarques que sans utiliser le "Sender", sr une fiche comportant plusieurs boutons, ce genre d'opération pourraît être complexe (étant entendu qu'on ne suppose pas connu le composant auquel la méthode est liée).

XVI-D-3. Surcharge et redéfinition des méthodes d'une classe

Nous avons déjà parlé de la possibilité, lors de la création d'une classe héritant d'une autre classe, de surcharger certaines de ses méthodes. La surcharge, signalée par le mot-clé override suivant la déclaration d'une méthode déjà existante dans la classe parente, n'est possible que sous certaines conditions :

  • Vous devez avoir accès à la méthode surchargée. Ainsi, si vous êtes dans la même unité que la classe parente, vous pouvez ignorer cette restriction. Sinon, en dehors de la même unité, la méthode doit être au minimum protégée.
  • La méthode de la classe parente doit être soit virtuelle (déclarée par la mot-clé virtual) soit déjà surchargée (override).

La surcharge possède également une propriété intéressante : elle permet d'augmenter la visibilité d'une méthode. Ainsi, si la méthode de la classe parente est protégée, il est possible de la surcharger dans la section protégée ou publique de la classe (la section publiée étant réservée aux propriétés). Depuis la même unité, une méthode surchargée présente dans la section privée de la classe parente peut être placée dans la section privée (pas de changement de visibilité), protégée ou publique (augmentation de visibilité). Il n'est pas possible de diminuer la visibilité d'une méthode surchargée. Cette fonctionnalité vous sera surtout utile au moment de la création de composants et pour rendre certaines méthodes, innaccessibles dans la classe parente, accessibles dans une ou plusieurs classes dérivées.

La surcharge au moyen du couple virtual-override permet ce qu'on appelle des appels virtuels de méthodes. En fait, lorsque vous appelez une méthode déclarée d'une classe et construire par une classe descendante de celle-ci, c'est la méthode surchargée dans cette dernière qui sera exécutée, et non la méthode de base de la classe de déclaration de l'objet. Voyons de suite un exemple histoire de clarifier cela. Pour faire fonctionner l'exemple, vous devez déclarer une méthode publique virtuelle appelée "affichage" dans la classe "AppareilAEcran". Voici sa déclaration :

 
Sélectionnez

  ...
    destructor Destroy; override;
    function affichage: string; virtual;
    property Poids: integer
    ...

et son implémentation :

 
Sélectionnez

function AppareilAEcran.affichage: string;
begin
  Result := 'Appareil à écran';
end;

Déclarez maintenant une méthode "affichage" surchargant celle de "AppareilAEcran" dans la classe "Television". Voici sa déclaration :

 
Sélectionnez

  ...
    constructor Create; override;
    function affichage: string;
    property Chaines[numero: integer]: integer
    ...

et son implémentation (Notez ici l'emploi d'une forme étendue de inherited que nous n'avons pas encore vue : il s'agit simplement de préciser le nom de la méthode surchargée, ce qui est obligatoire dés que l'on sort de l'appel simple de cette méthode, sans paramètres personnalisés et sans récupération du résultat. Le fait de récupèrer le résultat de la méthode héritée nons oblige à utiliser la forme étendue et non la forme raccourcie) :

 
Sélectionnez

function Television.affichage: string;
begin
  Result := inherited affichage;
  Result := Result + ' de type Télévision';
end;

Entrez maintenant le code ci-dessous dans une méthode associée à un clic sur un bouton. Exécutez-là et regardez ce qu'elle vous annonce.

 
Sélectionnez

procedure TfmPrinc.Button2Click(Sender: TObject);
var
  Tele1: AppareilAEcran;
  Tele2: AppareilAEcran;
begin
  Tele1 := AppareilAEcran.Create;
  Tele2 := Television.Create;
  ShowMessage('Tele1.affichage : ' + Tele1.affichage);
  ShowMessage('Tele2.affichage : ' + Tele2.affichage);
  ShowMessage('(Tele2 as AppareilAEcran).affichage : ' + (Tele2 as Television).affichage);
  Tele2.Destroy;
  Tele1.Destroy;
end;

Le premier appel indique un "appareil à écran". C'est normal puisque nous avons déclaré l'objet et l'avons construit à partir de cette classe. Les deux autres appels, par contre, signalent un "appareil à écren de type télévision". Le résultat du troisième affichage ne devrait pas vous étonner puisque nous demandons explicitement un appel de la méthode "affichage" pour l'objet "Tele2" en tant qu'objet de classe "Television". Le second affichage donne le même résultat, et c'est là que vous voyez le résultat d'un appel virtuel de méthode : l'objet "Tele2", pourtant déclaré de type "AppareilAEcran", lorsqu'on lui applique sa méthode "affichage", redonne bien l'affichage effectué par la classe "Television". C'est non seulement parce que nous avons créé l'objet avec la classe "Television" que cela se produit, mais aussi parce que les deux méthodes "affichage" des deux classes sont liées par une relation de surcharge.

Pour bien vous en rendre compte, supprimez le mot-clé override de la déclaration de "affichage" dans la classe "Television". Recompilez en ignorant l'avertissement du compilateur et relancez le test. Le premier affichage reste le même, puisque nous n'avons rien changé à ce niveau. Le second réserve la première surprise, puisqu'il signale maintenant un "appareil a écran" simple. En fait, nous avons appelé la méthode "affichage" dans un contexte où "Tele2" est de classe "AppareilAEcran". Comme le lien entre les deux méthodes "affichage" n'existe plus, il n'y a pas appel virtuel de la méthode "affichage" de "Television" mais seulement de "AppareilAEcran". Le troisième affichage est identique à celui de l'essai précédent. En effet, nous appelons cette fois-ci "affichage" dans un contexte où "Tele2" est de classe "Television". Cette méthode "affichage" fait appel à la méthode héritée, ce qui est en fait légitime même si elle ne la surcharge pas par override, puis complète le message obtenu par l'appel de la méthode héritée. C'est pour cela que le message est différent dans le deuxième et le troislème affichage.

Ce que nous venons de faire en supprimant le override de la déclaration de "affichage" n'est pas un abus mais une fonctionnalité de la programmation objet. Il s'agit non plus d'une surcharge mais d'une redéfinition de la méthode. Pascal Objet dispose même d'un mot-clé permettant de signaler cela et du même coup de supprimer le message d'avertissement du compilateur : reintroduce. Corrigez donc la déclaration de "affichage" dans "Television" comme ceci :

 
Sélectionnez

  ...
    constructor Create; override;
    function affichage: string; reintroduce;
    property Chaines[numero: integer]: integer
    ...

Recompilez et désormais le compilateur ne vous avertit plus. En effet, vous venez de lui dire : « OK, je déclare à nouveau dans une classe dérivée une méthode existante dans la classe parente, sans override, mais je ne veux pas la surcharger, je veux la redéfinir complètement ». Dans une méthode redéfinie, vous avez le droit d'appeler inherited mais ce n'est pas une obligation. Changez le code source de Television.affichage par ceci afin de vous en convaincre :

 
Sélectionnez

function Television.affichage: string;
begin
  Result := 'type Télévision';
end;

Le troisième affichage, lorsque vous relancez le test utilisé précédemment, signale bien un "type Télévision", ce qui prouve que les deux méthodes "affichage" sont maintenant indépendantes.

XVI-D-4. Méthodes abstraites

Les méthodes abstraites sont une notion très facile à mettre en oeuvre mais assez subtile à utiliser. Une méthode abstraite est une méthode virtuelle qui n'a pas d'implémentation. Elle doit pour cela être déclarée avec le mot réservé abstract. Le fait qu'une classe comporte une classe abstraite fait de cette classe une classe que dans d'autres langages on appelle classe abstraite mais qu'en Pascal Objet on n'appele pas. Une telle classe ne doit pas être instanciée, c'est-à-dire qu'on ne doit pas créer d'objets de cette classe.

L'utilité de ce genre de procédé ne saute pas immédiatement aux yeux, mais imaginez que vous deviez concevoir une classe qui serve uniquement de base à ses classes dérivées, sans avoir un quelconque intérêt en tant que classe isolée. Dans ce cas, vous devriez déclarer certaines méthodes comme abstraites et chaque classe dérivant directement de cette classe se devra d'implémenter la méthode afin de pouvoir être instanciée. C'est un bon moyen, en fait, pour que chaque classe descendante d'une même classe de base choisisse son implémentation indépendante des autres. Cette implémentation n'est pas une option, c'est une obligation pour que la classe dérivée puisse être instanciée.

Imaginez l'unité très rudimentaire suivante, où l'on définit une partie de trois classes :

 
Sélectionnez

unit defformes;

interface

uses
  SysUtils;

type
  TForme = class
  private
    X, Y: integer;
    function AfficherPosition: string;
  end;

  TRectangle = class(TForme)
  private
    Larg, Haut: integer;
  public
    function decrire: string;
  end;

  TCercle = class(TForme)
  private
    Rayon: integer;
  public
    function decrire: string;
  end;

implementation

{ TForme }

function TForme.AfficherPosition: string;
begin
  Result := 'Position : ('+IntToStr(X)+' , '+IntToStr(Y)+')';
end;

{ TRectangle }

function TRectangle.decrire: string;
begin
  Result := AfficherPosition+', Hauteur : '+IntToStr(Haut)+', Largeur : '+IntToStr(Larg);
end;

{ TCercle }

function TCercle.decrire: string;
begin
  Result := AfficherPosition+', Rayon : '+IntToStr(Rayon);
end;

end.

Vous constatez que chaque classe descendante de "TForme" doit définir sa propre méthode "decrire", ce qui est ennuyeux car la description est au fond un procédé très standard : on décrit la position, puis les attributs supplémentaires. Seuls les attributs supplémentaires ont besoin d'être fournis pour qu'une description puisse être faite. Nous pourrions envisager une seconde solution, déjà préférable :

 
Sélectionnez

unit defformes;

interface

uses
  SysUtils;

type
  TForme = class
  private
    X, Y: integer;
    function AfficherPosition: string;
    function AfficherAttributsSupplementaires: string; virtual;
  public
    function decrire: string;
  end;

  TRectangle = class(TForme)
  private
    Larg, Haut: integer;
    function AfficherAttributsSupplementaires: string; override;
  public
  end;

  TCercle = class(TForme)
  private
    Rayon: integer;
    function AfficherAttributsSupplementaires: string; override;
  public
  end;

implementation

{ TForme }

function TForme.AfficherPosition: string;
begin
  Result := 'Position : ('+IntToStr(X)+' , '+IntToStr(Y)+')';
end;

function TForme.AfficherAttributsSupplementaires: string;
begin
  Result := '';
end;

function TForme.decrire: string;
begin
  Result := AfficherPosition + AfficherAttributsSupplementaires;
end;

{ TRectangle }

function TRectangle.AfficherAttributsSupplementaires: string;
begin
  Result := Inherited AfficherAttributsSupplementaires +
            ', Hauteur : '+IntToStr(Haut)+', Largeur : '+IntToStr(Larg);
end;

{ TCercle }

function TCercle.AfficherAttributsSupplementaires: string;
begin
  Result := Inherited AfficherAttributsSupplementaires +
            ', Rayon : '+IntToStr(Rayon);
end;

end.

Dans cette nouvelle version, vous voyez que le code servant à la description a été regroupé et qu'il n'est plus nécessaire d'en avoir plusieurs versions qui risqueraient de partir dans tous les sens. Au niveau de "TForme", la description n'affiche que la position puisque "AfficherAttributsSupplementaires" retourne une chaîne vide. Par contre, au niveau des deux autres classes, cette fonction a été surchargée et retourne une chaîne spécifique à chaque classe. La description au niveau de ces classes appelera ces versions surchargées et décrira donc complètement un rectangle ou un cercle.

Le problème, ici, c'est qu'une classe descendante de "TForme" n'a pas du tout l'obligation de s'acquitter de sa description. En effet, elle peut fort bien ne pas surcharger "AfficherAttributsSupplementaires" et cela ne constituera pas une erreur. En outre, le fait d'avoir une implémentation de "AfficherAttributsSupplementaires" dans "TForme" est parfaitement inutile. Nous allons donc la transformer en méthode abstraite, que les classes descendantes DEVRONT surcharger pour avoir la prétention d'être instanciées correctement (une instance d'une classe comportant une méthode abstraite non surchargée plantera si on a le malheur d'appeler la méthode en question !). Voici le code source correspondant : notez que les inherited ont disparu et que l'implémentation de "AfficherAttributsSupplementaires" pour la classe "TForme" a également disparu :

 
Sélectionnez

unit defformes;

interface

uses
  SysUtils;

type
  TForme = class
  private
    X, Y: integer;
    function AfficherPosition: string;
    function AfficherAttributsSupplementaires: string; virtual; abstract;
  public
    function decrire: string;
  end;

  TRectangle = class(TForme)
  private
    Larg, Haut: integer;
    function AfficherAttributsSupplementaires: string; override;
  public
  end;

  TCercle = class(TForme)
  private
    Rayon: integer;
    function AfficherAttributsSupplementaires: string; override;
  public
  end;

implementation

{ TForme }

function TForme.AfficherPosition: string;
begin
  Result := 'Position : ('+IntToStr(X)+' , '+IntToStr(Y)+')';
end;

function TForme.decrire: string;
begin
  Result := AfficherPosition + AfficherAttributsSupplementaires;
end;

{ TRectangle }

function TRectangle.AfficherAttributsSupplementaires: string;
begin
  Result := ', Hauteur : '+IntToStr(Haut)+', Largeur : '+IntToStr(Larg);
end;

{ TCercle }

function TCercle.AfficherAttributsSupplementaires: string;
begin
  Result := ', Rayon : '+IntToStr(Rayon);
end;

end.

Cette version est de loin la meilleure. Nous avons cependant perdu une possibilité en gagnant sur la compacité du code source : il n'est plus envisageable d'instancier des objets de classe "TForme". Notez cependant que vous pouvez avoir des objets déclarés de classe "TForme" et construits (instanciés) par une des classes "TRectangle" ou "TCercle". Appeler la méthode "AfficherAttributsSupplementaires" pour de tels objets N'EST PAS une erreur, comme en témoigne l'exemple suivant :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
var
  F: TForme;
begin
  F := TRectangle.Create;
  ShowMessage(F.decrire);
  F.Destroy;
end;

dont le résultat est :

Image non disponible

Une petite explication et j'arrète de vous embêter : dans le code ci-dessus, l'objet F est déclaré de classe "TForme", mais construit de classe "TRectangle". Lorsque vous appelez "F.decrire", c'est d'abord la méthode "decrire" de "TForme" qui est recherchée et trouvée. Cette méthode fait appel à une première méthode classique qui ne pose aucun problème. L'appel de la seconde méthode est un appel d'une méthode abstraite, donc un appel voué à l'échec. La méthode "AfficherAttributsSupplementaires" est cependant virtuelle et c'est là que tout se joue : l'appel virtuel appelle non pas la méthode "AfficherAttributsSupplementaires" de "TForme" mais celle de la classe la plus proche de la classe de construction de l'objet F (soit "TRectangle") qui surcharge "AfficherAttributsSupplementaires" sans la redéfinir, c'est-à-dire dans ce cas précis "TRectangle". L'appel virtuel de "decrire" fait donc appel à la méthode "AfficherPosition" de "TForme" dans un premier temps et à la méthode "AfficherAttributsSupplementaires" de "TRectangle" dans un second temps.

Ne vous inquiètez pas trop si vous n'avez pas tout saisi : les méthodes virtuelles sont peu utilisées. En règle générale, on ne les trouve que dans le code source de certains composants.

XVI-D-5. Méthodes de classe

Nous allons conclure ce chapitre en parlant quelques instants de méthodes très particulières que nous avons déjà eu l'occasion d'utiliser sans les expliquer. Ces méthodes sont appelées méthodes de classe. Pour l'instant, nous avons employé deux de ces méthodes : le constructeur, "Create", et "ClassName".

Une méthode de classe est une méthode qui peut s'utiliser dans le contexte d'un objet ou, et c'est nouveau, d'une classe. Par contexte de classe, je veux parler de la forme :

 
Sélectionnez

... := TRectangle.Create;

Nous avions dit jusqu'à présent qu'une méthode s'appliquait à un objet, et manifestement, "Create" est appliquée à une classe. Il y a manifestement un souci. "Create" est en fait une méthode de classe, c'est-à-dire que, comme toutes les méthodes de classe, elle peut s'appliquer à un objet de la classe où elle a été définie, mais son intérêt est de pouvoir s'appliquer à la classe elle-même.

Essayez le code source ci-dessous :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage(TEdit.ClassName);
end;

qui doit afficher le message "TEdit". Comme vous pouvez le constater, nous n'avons pas construit d'instance de la classe "TEdit" mais nous avons cependant appelé sa méthode de classe ClassName qui retourne le nom de la classe, soit "TEdit".

En Pascal Objet, les méthodes de classe sont très rarement utilisées, mais je tenais simplement à vous montrer à quoi est due la forme bizarre que prend la construction d'un objet. A titre de curiosité, la déclaration de la méthode "ClassName" se fait dans la classe "TObject", et sa déclaration est :

 
Sélectionnez

  ...
    class function ClassName: ShortString;
    ...

Comme vous pouvez le constater, la déclaration d'une méthode de classe est précédée par le mot réservé class. Vous pourrez maintenant les repèrer si vous en rencontrer par hasard (et changer de trottoir rapidement ! ;-).

XVI-E. Conclusion

Ce chapitre est maintenant terminé. L'écriture m'en aura pris en temps incroyable, et pourtant il ne contient pas toutes les notions que j'avais envisagé d'y placer au départ. Au lieu de faire un pavé monolithique, j'ai préféré placer dans ce chapitre les notions les plus importantes à connaître dans le domaine de la programmation orientée objets sous Delphi, et relèguer à plus tard les notions suivantes :

  • Interfaces
  • Références de classe
  • Exceptions
  • Création de composants

Ces notions de plus en plus importantes en programmation objet feront l'objet d'un autre chapitre.


précédentsommaire

  

Copyright © 2008 Frédéric Beaulieu. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.