Introduction à la Programmation Orientée Objet25/07/2004
Par
Hdd34 (Programmation générale) Apprendre simplement la Programmation Orientée Objet Avant Propos 1. Vue d'ensemble de la POO 1.1. L'objet 1.2. Objet et classe 1.3. Les 3 fondamentaux de la POO 1.3.1. Encapsulation 1.3.2. Héritage 1.3.3. Polymorphisme 2. Différents types de méthodes 2.1. Constructeurs et destructeurs 2.1.1. Constructeurs 2.1.2. Destructeurs 2.2. Pointeur interne 2.3. Méthodes virtuelles et méthodes dynamiques 2.3.1. Méthodes virtuelles 2.3.1.1. Principe 2.3.1.2. Constructeurs et Table des Méthodes Virtuelles 2.3.2. Méthodes dynamiques 2.4. Méthodes abstraites 3. Visibilité 3.1. Champs et méthodes publics 3.1. Champs et méthodes privés 3.1. Champs et méthodes protégés 4. Le Pascal Objet 4.1. Déclaration d'un objet 4.1.1. Déclaration simple 4.1.2. Déclarations imbriquées 4.2. Champs et méthodes 4.2.1. Procédures et fonctions 4.2.2. Constructeurs 4.2.3. Destructeurs 4.2.4. Méthode virtuelles et dynamiques 4.2.5. Méthodes abstraites 4.2.6. Paramètre implicite Self 4.3. Visibilité 4.4. Héritage 4.4.1. Déclaration 4.4.2. Surcharge et appel de l'ancêtre 4.4.2.1. Ancêtre direct 4.4.2.2. Ancêtre indirect 4.4.3. Valeur de retour d'un constructeur 4.4.4. Ordre d'appel de l'ancêtre dans les constructeurs et destructeurs 4.5. Instanciation d'un objet 4.5.1. Instanciation statique 4.5.2. Instanciation dynamique 4.5.2.1. Allocation 4.5.2.2. Désallocation 4.6. L'objet générique TObject Conclusion
Dans ce tutoriel vous apprendrez à manier la Programmation Orientée Objet, ou POO pour les intimes,
dans le cadre du langage Pascal Orienté Objet. Loin d'être aussi complexe qu'elle peut le laisser transparaître, la POO peut se maîtriser rapidement au point de ne plus pouvoir s'en passer. Si la programmation dite procédurale est constituée de procédures et fonctions sans liens particuliers agissant sur des données dissociées pouvant mener rapidement à des difficultés en cas de modification de la structure des données, la programmation objet, pour sa part, tourne autour d'une unique entité : l'objet, offrant de nouvelles perspectives, et que je vous invite à découvrir de suite...
Avant de rentrer plus avant dans le sujet qui nous intéresse, nous allons commencer par poser un certain
nombre de bases.
Il est impossible de parler de Programmation Orientée Objet sans parler d'objet, bien entendu.
Tâchons donc de donner une définition aussi complète que possible d'un objet. Un objet est avant tout une structure de données. Autrement, il s'agit d'une entité chargée de gérer des données, de les classer, et de les stocker sous une certaine forme. En cela, rien ne distingue un objet d'une quelconque autre structure de données. La principale différence vient du fait que l'objet regroupe les données et les moyens de traitement de ces données. Un objet rassemble de fait deux éléments de la programmation procédurale :
Les champs et les méthodes d'un objets sont ses membres. Si nous résumons, un objet est donc un type servant à stocker des données dans des champs et à les gérer au travers des méthodes. Si on se rapproche du Pascal, un objet n'est donc qu'une extension évoluée des enregistrements (type record) disposant de procédures et fonctions pour gérer les champs qu'il contient.
Avec la notion d'objet, il convient d'amener la notion de classe. Cette notion de
classe n'est apparue dans le langage Pascal qu'avec l'avènement du langage Delphi et de sa
nouvelle approche de la Programmation Orientée Objet. Elle est totalement absente du Pascal
standard. Ce que l'on a pu nommer jusqu'à présent objet est, pour Delphi, une classe d'objet. Il s'agit donc du type à proprement parler. L'objet en lui-même est une instance de classe, plus simplement un exemplaire d'une classe, sa représentation en mémoire. Par conséquent, on déclare comme type une classe, et on déclare des variables de ce type appelées des objets. Si cette distinction est à bien prendre en considération lors de la programmation en Delphi, elle peut toutefois être totalement ignorée avec la plupart des autres compilateurs Pascal. En effet, ceux-ci ne s'appuient que sur les notions d'objet et d'instance d'objet. Nous adopterons par conséquent ici ce point vue, qui simplifie le vocabulaire et la compréhension. On pourra remarquer que FreePascal pour sa part définit une classe comme un "pointeur vers un objet ou un enregistrement".
La Programmation Orientée Objet est dirigée par 3 fondamentaux qu'il convient de toujours garder à
l'esprit : encapsulation, héritage et polymorphisme. Houlà ! Inutile de fuir en
voyant cela, car en fait, il ne cachent que des choses relativement simples. Nous allons tenter de
les expliquer tout de suite.
Derrière ce terme se cache le concept même de l'objet : réunir sous la même entité les données
et les moyens de les gérer, à savoir les champs et les méthodes. L'encapsulation introduit donc une nouvelle manière de gérer des données. Il ne s'agit plus de déclarer des données générales puis un ensemble de procédures et fonctions destinées à les gérer de manière séparée, mais bien de réunir le tout sous le couvert d'une seule et même entité. Si l'encapsulation est déjà une réalité dans les langages procéduraux (comme le Pascal non objet par exemple) au travers des unités et autres librairies, il prend une toute nouvelle dimension avec l'objet. En effet, sous ce nouveau concept se cache également un autre élément à prendre en compte : pouvoir masquer aux yeux d'un programmeur extérieur tous les rouages d'un objet et donc l'ensemble des procédures et fonctions destinées à la gestion interne de l'objet, auxquelles le programmeur final n'aura pas à avoir accès. L'encapsulation permet donc de masquer un certain nombre de champs et méthodes tout en laissant visibles d'autres champs et méthodes. Nous verrons ceci un peu plus loin. Pour conclure, l'encapsulation permet de garder une cohérence dans la gestion de l'objet, tout en assurant l'intégrité des données qui ne pourront être accédées qu'au travers des méthodes visibles.
Si l'encapsulation pouvait se faire manuellement (grâce à la définition d'une unité par exemple),
il en va tout autrement de l'héritage. Cette notion est celle qui s'explique le mieux
au travers d'un exemple. Considérons un objet Bâtiment. Cet objet est pour le moins
générique, et sa définition reste assez vague. On peut toutefois lui associer divers champs, dont
par exemple :
On peut supposer que cet objet Bâtiment dispose d'un ensemble de méthodes destinées à sa
gestion. On pourrait ainsi définir entre autres des méthodes pour :
Grâce au concept d'héritage, cet objet Bâtiment va pouvoir donner naissance à un ou
des descendants. Ces descendants vont tous bénéficier des caractéristiques propres de leur
ancêtre, à savoir ses champs et méthodes. Cependant, les descendants conservent la
possibilité de posséder leur propres champs et méthodes. Tout comme un enfant hérite des
caractéristiques de ses parents et développe les siennes, un objet peut hériter des
caractéristiques de son ancêtre, mais aussi en développer de nouvelles, ou bien encore
se spécialiser. Ainsi, si l'on poursuit notre exemple, nous allons pouvoir créer un objet Maison. Ce nouvel objet est toujours considéré comme un Bâtiment, il possède donc toujours des murs, un toit, une porte, les champs Adresse ou Superficie et les méthodes destinées par exemple à Ouvrir le Bâtiment. Toutefois, si notre nouvel objet est toujours un Bâtiment, il n'en reste pas moins qu'il s'agit d'une Maison. On peut donc lui adjoindre d'autres champs et méthodes, et par exemple :
Notre Bâtiment a ici bien évolué. Il s'est spécialisé. Avec notre Maison,
nous sommes allés plus avant dans les détails, et elle est à même de nous offir des services
plus évoluées. Nous avons complété ce qui n'était qu'un squelette. Ce processus d'héritage peut bien sûr être répété. Autrement dit, il est tout à fait possible de déclarer à présent un descendant de Maison, développant sa spécialisation : un Chalet ou encore une Villa. Mais de la même manière, il n'y a pas de restrictions théoriques concernant le nombre de descendants pour un objet. Ainsi, pourquoi ne pas déclarer des objets Immeuble ou encore Usine dont l'ancêtre commun serait toujours Bâtiment. Ce concept d'héritage ouvre donc la porte à un nouveau genre de programmation. On notera qu'une fois qu'un champ ou une méthode est définie, il ou elle le reste pour tous les descendants, quel que soit leur degré d'éloignement.
Le terme polymorphisme est certainement celui que l'on appréhende le plus. Mais il ne faut
pas s'arrêter à cela. Afin de mieux le cerner, il suffit d'analyser la structure du mot :
poly comme plusieurs et morphisme comme forme. Le polymorphisme
traite de la capacité de l'objet à posséder plusieurs formes. Cette capacité dérive directement du principe d'héritage vu précédemment. En effet, comme on le sait déjà, un objet va hériter des champs et méthodes de ses ancêtres. Mais un objet garde toujours la capacité de pouvoir redéfinir une méthode afin de la réécrire, ou de la compléter. On voit donc apparaître ici ce concept de polymorphisme : choisir en fonction des besoins quelle méthode ancêtre appeler, et ce au cours même de l'exécution. Le comportement de l'objet devient donc modifiable à volonté. Le polymorphisme, en d'autres termes, est donc la capacité du système à choisir dynamiquement la méthode qui correspond au type réel de l'objet en cours. Ainsi, si l'on considère un objet Véhicule et ses descendants Bateau, Avion, Voiture possédant tous une méthode Avancer, le système appellera la fonction Avancer spécifique suivant que le véhicule est un Bateau, un Avion ou bien une Voiture.
Parmi les différentes méthodes d'un objet se distinguent deux types de méthodes bien particulières
et remplissant un rôle précis dans sa gestion : les constructeurs et les destructeurs.
Comme leur nom l'indique, les constructeurs servent à construire l'objet en mémoire.
Un constructeur va donc se charger de mettre en place les données, d'associer les méthodes
avec les champs et de créer le diagramme d'héritage de l'objet, autrement dit de mettre en
place toutes les liaisons entre les ancêtres et les descendants. Il faut savoir que s'il peut exister en mémoire plusieurs instances d'un même type objet, autrement dit plusieurs variables du même type, seule une copie des méthodes est conservée en mémoire, de sorte que chaque instance se réfère à la même zone mémoire en ce qui concerne les méthodes. Bien entendu, les champs sont distincts d'un objet à un autre. De fait, seules les données diffèrent d'une instance à une autre, la "machinerie" reste la même, ce qui permet de ne pas occuper inutilement la mémoire. Certaines remarques sont à prendre en considération concernant les constructeurs.
Le destructeur est le pendant du constructeur : il se charge de détruire l'instance de
l'objet. La mémoire allouée pour le diagramme d'héritage est libérée. Certains
compilateurs peuvent également se servir des destructeurs pour éliminer de la mémoire le code
correspondant aux méthodes d'un type d'objet si plus aucune instance de cet objet ne réside en
mémoire. Là encore, différentes remarques doivent être gardées à l'esprit :
Très souvent, les objets sont utilisés de manière dynamique, et ne sont donc créés que lors
de l'exécution. Si les méthodes sont toujours communes aux instances d'un même type objet, il n'en
est pas de même pour les données. Il peut donc se révéler indispensable pour un objet de pouvior se référencer lui-même. Pour cela, toute instance dispose d'un pointeur interne vers elle-même. Ce pointeur peut prendre différentes appellations. En Pascal, il s'agira du pointeur Self. D'autres langages pourront le nommer this, comme le C++, ...
Une méthode dite virtuelle n'a rien de fictif ! Il s'agit d'une méthode dont la
résolution des liens est effectuée dynamiquement. Voyons ce que cela signifie. Comme nous le savons déjà, toute méthode est susceptible d'être surchargée dans un descendant, de manière à être écrasée ou complétée. Par conséquent, toute méthode surchargée donne lieu à création d'une nouvelle section de code, et donc à une nouvelle adresse en mémoire. De plus, tout objet possède un lien vers la table des méthodes de ses ancêtres : le diagramme d'héritage. De fait, tout type objet est directement lié à ses types ancêtres. Autrement dit, si nous reprenons l'exemple du début, l'objet Maison peut être assimilé à un Bâtiment. Considérons à présent la méthode Ouvrir d'un Bâtiment. Celle-ci consiste à ouvrir la porte principale. A présent, surchargeons cette méthode pour l'objet Maison, de sorte que la méthode Ouvrir non seulement ouvre la porte principale, mais également les volets de notre Maison. Déclarons maintenant une instance statique de Bâtiment, et appelons cette méthode Ouvrir. Lors de la création de l'exécutable, le compilateur va vérifier le type d'instance créé. Le compilateur lie alors notre appel à celui de Bâtiment.Ouvrir (la méthode Ouvrir de l'objet Bâtiment), en toute logique. Il ne se pose aucun problème. Considérons à présent un autre exemple : déclarons une variable dynamique destinée, en principe, à recevoir un objet Bâtiment. Comme nous l'avons vu juste avant, l'objet Maison est compatible avec l'objet Bâtiment. Comme nous travaillons en dynamique, nous nous servons de pointeurs. De fait, je peux très bien décider, avec cette variable pointant vers un objet Bâtiment, de déclarer une instance de type Maison : le compilateur n'opposera aucune réticence. Si nous résumons, nous avons donc une variable de type officiel pointeur vers Bâtiment et contenant en réalité une Maison. Appelons alors notre méthode Ouvrir. Comme nous avons une Maison, il faut que l'on ouvre les volets. Or, si nous exécutons notre programme, les volets resteront clos. Que s'est-il passé ? Lors de la création du programme, le compilateur s'est arrêté sur notre appel à Ouvrir. Ayant déclaré un objet Bâtiment, le compilateur ignore tout du comportement du programme lors de son exécution, et par conséquent ignore que la variable de type pointeur vers Bâtiment contiendra à l'exécution un objet Maison. De fait, il effectue une liaison vers Bâtiment.Ouvrir alors que nous utilisons une Maison ! La solution, vous l'aurez compris, réside dans l'utilisation des méthodes virtuelles. Grâce à celles-ci, la résolution des liens est effectuée dynamiquement, autrement dit lors de l'exécution. Ainsi, si nous déclarons notre méthode Ouvrir comme virtuelle, lors de la création du programme, le compilateur n'effectuera aucune liaison statique devant notre appel. Ce n'est que lors de l'exécution, au moment de l'appel, que la liaison va s'effectuer. Ainsi, au moment où l'on désirera appeler Ouvrir, notre programme va interroger son pointeur interne pour déterminer son type. Bien évidemment, cette fois-ci, il va détecter une instance de Maison, et l'appel se fera donc en direction de Maison.Ouvrir. Les volets s'ouvrent... Vous aurez noté toute l'importance des méthodes virtuelles. D'une manière générale, sitôt qu'une méthode est susceptible d'être surchargée, il faut la déclarer comme virtuelle.
Afin de pouvoir appeler la méthode approprié au moment souhaité, un objet doit s'appuyer
sur une liste de ses méthodes virtuelles : la VMT ou Virtual Methods
Table, la Table des Méthodes Virtuelles. Cette table est mise en place par les
constructeurs d'un objet. Tout objet possède sa propre VMT, conservant toujours
un lien avec la VMT de son ancêtre. Lorsqu'un appel à une méthode virtuelle est effectuée, l'objet recherche dans sa VMT s'il trouve la méthode recherchée. Si c'est le cas, alors il utilise l'adresse enregistrée et exécute la méthode. Sinon, il parcourt la VMT de son ancêtre direct et ainsi de suite jusqu'à l'ancêtre le plus éloigné dans la hiérarchie. De même, lorsque qu'une méthode surchargée fait appel à la méthode ancêtre, alors une recherche est effectuée en partant cette fois-ci de la VMT du premier ancêtre. La VMT est détruite par un destructeur lorsque celle-ci n'a plus lieu d'exister.
Après les méthodes virtuelles, on se demande ce que l'on a pu inventer de pire ! Rassurez-vous,
rien du tout. Les méthodes dynamiques ne sont en fait que des méthodes virtuelles. Leur particularité réside dans le fait qu'elles sont indexées. Autrement dit, chaque méthode dynamique possède un numéro unique pour l'identifier. Il convient de les comparer aux méthodes virtuelles :
Par conséquent, on préfèrera toujours les méthodes virtuelles, sauf si de nombreuses
méthodes virtuelles doivent être déclarées, auquel cas on se reportera aux méthodes
dynamiques.
Une méthode abstraite est une méthode qu'il est nécessaire de surcharger. Elle ne
possède donc pas d'implémentation. Ainsi, si on tente d'appeler une méthode abstraite,
alors une erreur est déclenchée. Bien entendu, il convient lors de la surcharge d'une telle méthode de ne pas faire appel à la méthode de l'ancêtre... Les méthodes abstraites sont généralement utilisées lorsque l'on bâtit un squelette d'objet devant donner lieu à de multiples descendants devant tous posséder un comportement analogue. On pourra prendre notamment l'exemple de l'objet TStream et de tous ses descendants.
De par le principe de l'encapsulation, afin de pouvoir garantir la protection des données, il convient de
pouvoir masquer certaines données et méthodes internes les gérant, et de pouvoir laisser
visibles certaines autres devant servir à la gestion publique de l'objet. C'est le principe
de la visibilité.
Comme leur nom l'indique, les champs et méthodes dits publics sont accessibles depuis tous les
descendants et dans tous les modules : programme, unité, ... On peut considérer que les éléments publics n'ont pas de restriction particulière. Les méthodes publiques sont communément appelées accesseurs : elles permettent d'accéder aux champs d'ordre privé. Il existe des accesseurs en lecture, destinés à récupérer la valeur d'un champ, et des accesseur en écriture destinés pour leur part à la modification d'un champ. Il n'est pas nécessaire d'avoir un accesseur par champ privé, car ceux-ci peuvent n'être utilisés qu'à des fins internes. très souvent, les accesseurs en lecture verront leur nom commencer par Get quand leurs homologues en écriture verront le leur commencer par Set ou Put.
La visibilité privée restreint la portée d'un champ ou d'une méthode au module où il ou
elle est déclaré(e). Ainsi, si un objet est déclaré dans une unité avec un champ privé, alors
ce champ ne pourra être accédé qu'à l'intérieur même de l'unité.
Généralement, les accesseurs, autrement dit les méthodes destinées à modifier les champs,
sont déclarés comme privés.
La visibilité protégé correspond à la visibilité privé excepté que tout champ ou
méthode protégé(e) est accessible dans tous les descendants, quel que soit le module où il se
situent.
Tous les éléments de la Programmation Orientée Objet énoncés jusqu'ici sont bien entendu supportés par
le Pascal Objet. Nous allons voir à présent comment les implémenter.
Avant de pouvoir utiliser la POO, il convient de savoir déclarer un objet. Pour ce faire, on a
recours au mot réservé object. Un objet étant une structure de données, il sera donc
toujours déclaré à l'intérieur d'un bloc type. L'objet le plus basique que l'on puisse déclarer est tout simplement l'objet vide, qui n'effectue rien et ne contient rien. On le déclare comme ceci :
On remarquera que la déclaration d'un objet se terminera toujours par un
end;. De plus, on adopte très souvent en Pascal une convention de notation pour les objets : leur nom commence toujours par T, comme type. Nous respecterons toujours cette convention dans ce tutoriel, et nous parlerons donc des objets TMaison ou encore TChose.
Dans la suite de ce tutoriel, nous adopterons l'utilisation de mot réservé object. Celui-ci
sera à remplacer en fonction du compilateur utilisé (Delphi par exemple).
Il est parfois nécessaire de déclarer des objets qui s'utilisent mutuellement. On peut ainsi
prendre l'exemple de deux objets, TParent et TEnfant, le parent ayant la
nécessité de connaître la liste de ses enfants, et l'enfant la nécessité de connaître son
parent. Intervient alors le problème de la déclaration imbriquée de deux objets. Si l'on tente de les déclarer comme ceci :
Le compilateur va déclencher une erreur indiquant qu'il connaît pas encore
TEnfant lorsqu'il tente d'analyser la structure de TParent. La solution au problème passe nécessairement par l'instanciation dynamique des deux objets (voir le paragraphe concerné). On va donc déclarer deux pointeurs vers les deux types considérés et seulement ensuite, on déclarera les objets eux-même, ceci dans le même bloc type :
Grâce à cette méthode, plus aucune erreur n'est déclenchée, car lorsque le compilateur va
analyser TParent, il aura déjà eu connaissance de l'existance de PEnfant. Ceci
n'est bien évidemment possible que parce que le compilateur accepte la déclaration
prématurée de pointeurs avant le type vers lequel ils pointent.
Un objet vide ne présentant pas d'intérêt majeur, il pourrait être intéressant de savoir comment
lui ajouter des champs et des méthodes.
Comme à l'intérieur d'un enregistrement de type record, les champs se déclarent comme de
simples variables, sans réutiliser le mot réservé var à l'intérieur de la déclaration de
l'objet. Tous les types peuvent être utiliséspour un champ. Ainsi, les exemples suivants sont
tous valides :
On pourra ainsi accéder aux champs de la manière suivante :
Les méthodes se déclarent à la suite des champs. Le schéma théorique actuel de description d'un objet
serait ainsi :
Les méthodes - procédures et fonctions - se déclarent à l'intérieur de l'objet comme on le ferait
dans la partie interface d'une unité :
Une fois ces méthodes déclarées, il faut écrire le code source associé. On complète leur
déclaration en-dehors de la déclaration de l'objet. Le nom de la méthode est alors
précédé du nom de l'objet suivi d'un point. Un exemple expliquant aussi bien, voici donc
ce que donnerait l'ajout d'une méthode Methode1 :
Comme il s'agit d'une déclaration de type forward, on peut éventuellement omettre les
paramètres lorsque l'on complète la déclaration d'une méthode :
Ce code et le code précédent sont tous deux parfaitement identiques. On choisira donc de
privilégier soit la facilité de lecture, soit la simplicité d'écriture.
Un constructeur se déclare exactement comme une autre méthode. Il n'y a pas d'ordre particulier,
et on peut très bien intercaler des constructeurs au milieu des autres méthodes. Le mot réservé
procedure (ou function) sera ici remplacé par le mot
réservé constructor :
L'usage veut que le constructeur principal d'un objet soit appelé Init ou Create.
Il n'y a aucune obligation dans ce domaine, mais on essaie la plupart du temps de suivre cette
convention.
Il est possible de faire échouer un constructeur, par exemple si une opération nécessaire à
l'initialisation de l'objet s'est mal déroulée. En fonction du compilateur, soit on déclenchera une exception (Delphi), soit on fera appel à la procédure Fail :
De même que pour les constructeurs, on se servira ici du mot réservé destructor :
Ici encore, l'usage veut que l'on appelle les destructeurs Done ou Destroy. De plus,
la plupart du temps, un destructeur n'aura pas de paramètres. Toutefois, le contraire est tout à
fait autorisé par le compilateur.
Si vous désirez déclarer une méthode virtuelle, alors vous devez ajouter à la déclaration
de la méthode (procédure, fonction, constructeur ou destructeur) le mot réservé virtual
suivi d'un point virgule. virtual ne devra pas être repris lorsque
vous complèterez le code de la méthode :
Les méthodes dynamiques sont également appelées méthodes virtuelles indexées. En
effet, rien ne les distingue des méthodes virtuelles si ce n'est qu'elles possèdent un index,
un numéro pour les identifier. La déclaration d'une méthode dynamique varie d'un compilateur à un autre. Sur les compilateurs plus anciens, elles seront déclarées comme ceci :
Où IndexUnique représente un entier unique permettant d'identifier la méthode
virtuelle.
La déclaration d'une méthode abstraite dépend du compilateur utilisé. Si vous utilisez un
compilateur Pascal d'ancienne génération, alors, bien qu'une méthode abstraite ne doit en théorie
pas posséder d'implémentation, celle-ci se déclarera comme toute autre méthode, et dans le corps
de la méthode, on ajoute un appel à la procédure Abstract; :
Dès lors qu'une méthode est appelée, le compilateur lui fournit implicitement, autrement
dit de manière non visible, un paramètre supplémentaire, Self, que vous n'avez donc pas à
gérer. Le paramètre Self représente l'instance de l'objet en cours, et possède donc le même type que l'objet. Ainsi, si un objet possède un champ nommé Toto, alors les deux codes suivants seront identiques :
Le paramètre Self sert de manière interne à l'objet pour garantir un appel correct des
méthodes virtuelles. Pour le programmeur, Self peut aussi servir à vérifier qu'une instance d'un même type d'objet est déjà en mémoire et, par exemple, interdire la création d'une nouvelle instance :
La visibilité de champs et méthodes s'indique grâce à divers mots réservés :
Le spécificateur de visibilité doit être placé avant l'ensemble des champs et méthodes devant
bénéficier de cette visibilité :
Les spécificateurs peuvent être placés dans un ordre quelconque, et apparaître plusieurs fois si
nécessaire, bien que cette possibilité soit rarement utilisée.
Un objet en Pascal ne peut hériter que d'un seul ancêtre. Afin de spécifier celui-ci, on spécifie
son nom entre parenthèses après le mot réservé object :
Sitôt qu'un ancêtre est déclaré pour un objet, ce dernier peut accéder à tous les champs et
méthodes de son ancêtre sans avoir à les redéclarer (en fonction de la visibilité).
La plupart du temps, lorsque que l'on surcharge une méthode, le but n'est pas d'écraser
l'ancienne, mais de la compléter de façon à apporter de nouvelles fonctionnalités.
Il est donc nécessaire de pouvoir appeler la méthode ancêtre. Deux cas de figure sont alors à envisager.
Généralement, on appelle l'ancêtre direct, celui dont on hérite en première main. Pour
appeler la méthode ancêtre, on utilise alors le mot réservé inherited devant le
nom de la méthode, à l'endroit on l'on désire effectuer l'appel :
La méthode ancêtre peut être appelée autant de fois que désiré, et on peut effectuer un
nombre quelconque d'opérations avant et/ou après ce ou ces appels.
Dans certains cas particuliers, il peut être nécessaire d'appeler un ancêtre plus éloigné
dans la hiérarchie : par exemple l'ancêtre de l'ancêtre direct. Dans ce cas, il faut faire
explicitement appel à la méthode en faisant précéder son nom par le nom de l'ancêtre suivi
d'un point. Dans ce cas, les méthodes surchargées apparaissant entre l'objet et l'ancêtre
appelé seront ignorées.
Lorsque que le constructeur d'un ancêtre est appelé avec inherited, celui-ci renvoie une
valeur booléenne indiquant si la construction de l'objet s'est effectuée sans erreur. Ainsi, si la construction a échoué dans l'ancêtre (par un appel à Fail notamment), alors il convient de faire échouer également la construction du descendant, celui-ci ne pouvant fonctionner sans le support de son ancêtre. Dès lors, il faudra aussi faire un appel à Fail. Généralement, on adopte la syntaxe suivante :
S'il est possible de faire appel à inherited où on le souhaite et le nombre de fois
désiré dans une méthode, les constructeurs et les destructeurs sont régis par des règles plus
strictes qui ne seront pas vérifiées par la compilateur. Elles sont donc d'autant plus
importantes.
Une fois l'objet déclaré - autrement dit le type défini - il reste à créer une ou plusieurs instances
de celui-ci, avec des variables : c'est l'instanciation, ou plus simplement la création.
Il en existe de deux sortes : l'instanciation statique et l'instanciation dynamique.
L'instanciation statique est certainement la plus simple à mettre en oeuvre, mais aussi
celle à éviter le plus possible. Elle consiste à simplement déclarer une variable du type
objet comme on déclarerait n'importe quelle variable :
Si l'objet possède un constructeur, celui-ci devra être appelé avant toute autre méthode,
et de même, si l'objet possède un destructeur, il devra être appelé en dernier.
L'instanciation dynamique est certainement la plus utilisée et celle permettant de stocker
en mémoire le plus d'objets et d'information. Elle suppose bien entendu une connaissance des
pointeurs. Afin de pouvoir utiliser le dynamique, il faut commencer par déclarer un pointeur vers le type objet que l'on projette d'utiliser. L'usage veut que cette déclaration se fasse conjointement avec celle de l'objet en lui-même. De même, on choisit généralement de faire figurer la déclaration du pointeur juste avant celle de l'objet. Le nom du pointeur pourra reprendre celui de l'objet, en remplaçant le T initial par un P. Par exemple, si l'on considère l'objet TObjet, alors le pointeur vers l'objet sera nommé communément PObjet.
L'allocation mémoire de l'objet et son initialisation se font généralement en même
temps à l'aide de la fonction standard New. Deux formes peuvent être utilisées :
Ou :
Tout comme pour l'allocation, la désallocation d'effectue généralement en même temps que
l'appel du destructeur de l'objet à l'aide de la procédure Dispose. Le destructeur
est alors passé avec ses éventuels paramètres comme deuxième paramètre de la procédure.
Turbo Pascal et la plupart des autres compilateurs définissent un objet générique servant de
base à tous les objets de la bibliothèque standard : c'est TObject. Cet objet, placé dans
l'unité Objects, est déclaré comme ceci :
On pourra donc prendre pour habitude de dériver tous ses objets de base de TObject, car il
fournit une architecture de base pratique et importante pour la compatibilité avec les objets
de la bibliothèque standard. L'allocation et la désallocation d'un tel objet deviendront alors :
Vous avez appris ici les bases de la Programmation Orientée Objet et vous êtes à présent à même de
construire vous-même vos propres programmes objets. Seule la pratique permettant de faire des progrès, à vos claviers ! Remerciements particuliers à Laurent Dardenne et Richard Gaillard.
Copyright (C) 2004, Eric Sigoillot, Tous droits réservés
|