Programmation objet

Notions de programmation objet

Auteur

Eric Thirion

Dans ce deuxième cours sur la programmation objet, vous apprendrez à définir vous-même de nouvelles classes, à écrire des méthodes ou à définir des relations d'héritage entre classes.

Nous aborderons également de nouvelles notions de programmation objet :

- l'encapsulation : une méthodologie de programmation qui facilite la mise à jour des programmes écrits sous forme de classes ;

- les propriétés : une approche particulière de l'encapsulation de Pascal Objet. Nous verrons en particulier que certains attributs graphiques de Lazarus (comme l'attribut .'text' des zones de texte) sont en réalité des propriétés et que cela permet d'expliquer comment la modification de la valeur de ces attributs se répercute immédiatement à l'écran ;

- la surcharge ou comment redéfinir spécifiquement une méthode pour une sous-classe en récupérant le code de la classe-mère ;

- le polymorphisme : comment faire passer un objet d'une classe donnée comme un objet d'une sous-classe de celle-ci.

Commentez

- Titre : Programmation objet

- Auteur : Eric Thirion

- Parution : 18 octobre 2014

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Eric Thirion. 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.

Sommaire

- Utilisation de classes et interfaces graphiques

- Notions de programmation objet

- Pré-requis

- Définition basique d'une classe

- Présentation du projet

- Définition de la classe et de ses méthodes

- Interface de la classe Voiture

- Implémentation de la classe Voiture

- En-têtes des méthodes

- Corps des méthodes

- Utilisation des méthodes de la classe

- Le bouton Nouvelle Voiture

- Le bouton Rouler

- Encapsulation

- Visibilité des membres d'une classe

- Visibilité des membres en Pascal Objet

- Présentation de l'exemple

- Déclaration de la classe

- Utilisation interdite de la classe

- Utilisation autorisée de la classe

- Les propriétés

- Utilité des propriétés

- Déclaration des propriétés

- Déclaration des propriétés simples

- Déclaration des propriétés tableaux

- Exemple de propriété simple

- Exemple de propriété tableau

- Déclaration et utilisation de la propriété En_Stock

- Interface de la classe Stock

- Implémentation des accesseurs de la méthode En_Stock

- Utilisation de la classe Stock

- Affichage du stock

- Propriétés graphiques

- Héritage, surcharge et polymorphisme

- Présentation de l'exemple

- Interfaces des deux classes

- Implémentation de la classe Forme

- Implémentation de la classe Cercle

- Utilisation des deux classes

- Affichage d'une forme

- Création d'un cercle

- Exercices et exemples

Utilisation de classes et interfaces graphiques

Notions de programmation objet

Pré-requis

Pour comprendre ce cours, il est nécessaire d'avoir acquis la partie Utilisation de classes du premier cours sur la programmation objet.

Définition basique d'une classe

L'exemple de projet qui va nous servir à illustrer la définition basique d'une classe se trouve dans le dossier Exemple-ProgObjet2/Voiture1. Dans ce projet, nous avons défini une classe pour représenter une voiture.

Présentation du projet

Voici l'interface graphique de ce programme :

Le bouton Nouvelle Voiture crée une nouvelle instance de la classe voiture, en lisant son immatriculation depuis la zone de texte prévue à cet effet.

Le Bouton Rouler augmente le compteur par la distance indiquée par l'utilisateur, sauf si le compteur atteint ou dépasse 10 000 km. Dans ce cas, le programme affiche un message indiquant que les pneus de la voiture sont usés.

Définition de la classe et de ses méthodes

En Pascal Objet, la définition d'une classe se fait en deux parties :

- définition de l'interface de la classe : elle contient la définition des attributs de la classe, et les en-têtes (ou signatures) de ses méthodes. Elle figure dans la partie Interface du fichier ;

- l'implémentation de la classe : c'est la définition du code des méthodes de la classe. Elle figure dans la partie Implementation du fichier.

Interface de la classe Voiture

Voici l'interface de la classe Voiture :

Compteur et Immat sont les attributs de la classe.

Nouvelle, Initialiser, Rouler et AfficherAttributs sont les méthodes de la classe.

Rouler et AfficherAttributs sont des procédures, ChangerPneu est une fonction et la méthode Nouvelle est une méthode spéciale dont la présence n'est pas obligatoire et que l'on appelle le constructeur de la classe. Nous verrons quel est son rôle un peu plus loin.

Comme vous pouvez le constater, le code des méthodes ne figure pas dans l'interface. On y met simplement les en-têtes.

Implémentation de la classe Voiture

Voici l'implémentation de la classe Voiture :

- La méthode Nouvelle est le constructeur de la classe. Elle permet de créer une nouvelle instance de la classe Voiture en donnant une valeur à son immatriculation (la valeur du paramètre Im). Sans constructeur, il aurait été nécessaire d'utiliser la méthode Create, qui fonctionne pour n'importe quelle classe. Mais Create ne permet pas d'initialiser immédiatement les attributs de l'instance générée. Le constructeur est donc une sorte de méthode Create paramétrisée.

- La méthode Rouler augmente le compteur d'une voiture du nombre de kilomètres défini par le paramètre Km, sauf s'il est nécessaire de changer les pneus. Dans ce dernier cas, elle affiche le message « Vos pneus sont usés ! ».

- La méthode ChangerPneu est une fonction booléenne qui retourne la valeur true si et seulement si le nombre de kilomètres de la voiture est supérieur ou égal à 10 000.

- La méthode AfficherAttribut affiche les valeurs des attributs d'une voiture (immatriculation et compteur) dans les zones de texte prévues à cet effet.

En-têtes des méthodes

Les en-têtes des méthodes sont identiques aux en-têtes définies dans l'interface, sauf que le nom de chaque méthode est précédé du nom de la classe : Voiture.Nouvelle, Voiture.Rouler, Voiture.ChangerPneu…

C'est de cette manière que l'on associe une méthode à sa classe dans la partie Implementation.

Les méthodes qui commencent par « Voiture. » sont celles de la classe Voiture.

Corps des méthodes

Dans le corps des méthodes, on remarque que les attributs de la classe ainsi que les appels de méthodes de la classe ne sont pas précédés du nom d'une instance de cette classe.

Par exemple, dans le corps du constructeur, les attributs Immat, Compteur ne sont pas précédés par le nom d'une instance.

Dans la méthode Rouler, on appelle les méthodes ChangerPneu et Afficher. Mais elles ne sont apparemment appliquées à aucune instance de la classe Voiture !?

En fait, cette instance est implicite : c'est l'instance à laquelle sera appliquée la méthode. On ne la connaît donc pas lorsque l'on écrit le code d'une méthode. Dans tout le corps d'une méthode, il faut donc imaginer que le nom de cette instance figure à gauche des attributs et des méthodes de la classe.

Par défaut, les attributs non précédés du nom d'une instance qui figurent dans le corps d'une méthode de la classe Voiture sont interprétés comme des attributs de la classe Voiture.

C'est la raison pour laquelle les zones de textes Zt_Immat et Zt_Compteur sont précédés par Form1 dans le corps de la procédure AfficherAttribut.

Sinon, ils auraient été interprétés comme des attributs de la classe Voiture, ce qui aurait généré une erreur de compilation.

Rappelons nous (voir cours I sur la programmation objet) que Form1 est une instance de la classe TForm1 qui représente le formulaire d'une application. Les attributs de cette instance sont les différents composants du formulaire. En particulier, la zone de texte Zt_Immat, doit donc être désignée par Form1.Zt_Immat en dehors d'une méthode de la classe TForm1.

Utilisation des méthodes de la classe

Les méthodes de la classe Voiture sont utilisées dans les différentes procédures évènementielles de l'application. Les voici :

Nous avons déclaré une variable nommée LaVoiture de type Voiture. Elle nous servira à mémoriser une instance (unique dans ce programme) de la classe Voiture.

Les procédures évènementielles sont des méthodes de la classe TForm1. C'est la raison pour laquelle elles sont toutes préfixées par TForm1.

Le bouton Nouvelle Voiture

Bt_CreerClick est la procédure évènementielle associée au bouton Nouvelle Voiture.

Dans le corps de cette procédure, on commence par lire l'immatriculation depuis la zone de texte Zt_Immat. Cette chaîne de caractères est affectée à la variable locale immat.

Remarquez qu'elle n'est pas préfixée ici par Form1., car on se trouve dans une méthode de la classe TForm1.

L'instruction suivante génère une instance de Voiture avec immat comme immatriculation, en faisant appel à la méthode Nouvelle, c'est-à-dire au constructeur de la classe.

Puis les attributs de la nouvelle voiture sont affichés en appliquant la méthode AfficherAttributs à l'instance LaVoiture.

Le bouton Rouler

Bt_RoulerClick est la procédure évènementielle associée au bouton Rouler.

Dans le corps de cette procédure, on commence par lire la distance à parcourir depuis la zone de texte Zt_Distance. Cet entier est ensuite affecté à la variable locale d.

Ensuite, on fait rouler la voiture pendant d kilomètres en appliquant la méthode Rouler à LaVoiture.

Remarquez que l'on suppose ici que l'instance de voiture a été générée, c'est-à-dire que l'utilisateur a cliqué sur le bouton Nouvelle Voiture avant de cliquer sur le bouton Rouler.

Dans le cas contraire on obtiendrait une erreur d'exécution, car LaVoiture ne contiendrait pas l'adresse d'une instance de voiture.

Encapsulation

L'encapsulation est une manière de définir une classe de telle sorte que ses attributs ne puissent pas être directement manipulés de l'extérieur, mais seulement indirectement par l'intermédiaire de méthodes spéciales appelées accesseurs.

Un des avantages de cette approche est la possibilité de redéfinir la représentation interne des attributs, sans que cela affecte la manipulation de la classe via les méthodes.

Autrement dit, les programmes utilisant le principe d'encapsulation sont plus facile à mettre à jour.

Visibilité des membres d'une classe

Un membre d'une classe désigne un attribut ou une méthode définie dans cette classe.

La visibilité des membres d'une classe définit les endroits d'où ils peuvent être utilisés. Dans cette section, nous allons voir deux sortes de visibilité : la visibilité publique et la visibilité privée.

Les membres privés d'une classe ne sont accessibles que par les méthodes de la classe :

- les attributs privés d'une classe ne peuvent être utilisés que par les méthodes de cette classe ;

- les méthodes privées d'une classe ne peuvent être appelées que par les méthodes de cette classe.

Les membres publics d'une classe sont accessibles de l'extérieur de la classe :

- les attributs publics d'une classe sont accessibles (en lecture ou en écriture) depuis n'importe quelle partie du programme ;

- les méthodes publiques d'une classe peuvent être appelées depuis n'importe quelle partie du programme.

Une classe est encapsulée si tous ses attributs sont privés.

Visibilité des membres en Pascal Objet

Pascal Objet offre cinq types de visibilité : Private, Strict Private, Protected, Public et Published. Si rien n'est précisé, c'est implicitement la visibilité Public qui est utilisée.

Je ne parlerai ici que de Strict Private et Published, qui correspondent aux visibilités privée et publique dont j'ai parlé précédemment.

Juste un mot sur Private. On pourrait croire d'après le nom qu'elle correspond à la visibilité privée. Mais non !

La visibilité Private de Pascal n'est pas réellement privée car elle autorise des accès depuis n'importe quel sous-programme ou méthode se trouvant dans la même unité.

Présentation de l'exemple

Pour illustrer le principe d'encapsulation, nous allons reprendre la classe Voiture en définissant la visibilité de ses membres de telle manière qu'elle soit encapsulée. Il s'agit donc d'une nouvelle version du projet Voiture1, que vous trouverez dans le dossier Exemple-ProgObjet2/Voiture2.

L'interface graphique de ce nouveau projet est exactement la même que celle du projet Voiture1 :

De plus, les deux projets font exactement la même chose. Ils ne diffèrent que par le code.

Le projet comporte a présent deux unités :

- l'unité Voiture2.pas contient les procédures évènementielles associées à l'interface graphique ;

- l'unité ClasseVoiture2.pas contient la définition de la classe Voiture.

L'unité Voiture2.pas utilise la classe Voiture définie dans l'unité ClasseVoiture2.pas.

Dans Voiture2.pas, nous donnons deux utilisations de la classe :

- la première est une utilisation interdite, ne respectant pas la visibilité des membres de la classe. Elle provoquera une erreur de compilation ;

- la deuxième respecte la visibilité des membres et ne provoque pas d'erreur de compilation.

L'une ou l'autre version peut être obtenue en mettant ou en enlevant les commentaires dans les procédures AfficherLaVoiture et TForm1.Bt_CreerClick.

Déclaration de la classe

Voici la nouvelle interface de la classe voiture :

Conformément au principe d'encapsulation, les attributs sont privés.

Dans cet exemple, toutes les méthodes sont publiques. Mais certaines d'entre elles auraient pu être privées sans enfreindre le principe d'encapsulation.

Pour pouvoir mieux illustrer nos propos, nous avons enlever les méthodes Rouler, ChangerPneu et AfficherAttributs ainsi que le constructeur de la classe.

Les nouvelles méthodes, getCompteur, setCompteur, getImmat et setImmat sont les accesseurs de la classe. Ce sont eux qui vont nous permettre d'agir sur une instance de la classe depuis l'unité Voiture2.

Dans notre exemple, extrêmement simplifié, les accesseurs sont les seules méthodes de la classe. En général, ce n'est pas le cas.

Il y a deux types d'accesseurs :

- les accesseurs en lecture : ils permettent de lire la valeur d'un attribut. Ce sont des fonctions. Par convention, leur nom commence par get ;

- les accesseurs en écriture : ils permettent de modifier la valeur d'un attribut. Ce sont des procédures. Par convention, leur nom commence par set.

Dans notre exemple, nous avons donc un accesseur en lecture et un accesseur en écriture pour chacun des attributs de la classe.

Voici le code de ces méthodes :

Dans les accesseurs en écriture, la valeur à affecter à l'attribut est passée en paramètre.

Dans le corps de l'accesseur, il peut être intéressant de n'affecter cette valeur que si elle vérifie certaines conditions. On peut de cette manière éviter des opérations inadéquates sur une instance de la classe.

Dans notre exemple, nous avons fait un contrôle de ce type dans l'accesseur setCompteur : lorsque la valeur passée en paramètre est supérieure à 10 000, elle n'est pas affectée à l'attribut Compteur. Un message d'erreur est affichée.

Utilisation interdite de la classe

Voici une première version du module Voiture2.pas, dans laquelle on essaie d'accéder directement aux membres privés de la classe voiture :

Comme on pouvait s'y attendre, cette version provoque une erreur de compilation.

Par contre, on constate que si on rend les attributs Immat et Compteur publics, le projet devient compilable et fonctionne très bien.

Utilisation autorisée de la classe

Pour qu'une classe encapsulée puisse être utilisée, il est nécessaire d'accéder à ses attributs de manière indirecte, via les accesseurs de la classe.

Voici une deuxième version du module Voiture2.pas, utilisant la classe Voiture encapsulée :

On constate que la compilation se passe bien et que le projet fonctionne correctement.

Les propriétés

En plus des attributs et des méthodes, Pascal Objet offre la possibilité de définir des propriétés. Nous verrons qu'elles permettent d'encapsuler une classe d'une manière intéressante.

C'est avec les propriétés, également, que nous allons pouvoir (enfin !) expliquer le comportement étrange de certains « attributs » (en fait ce sont des propriétés) des composants graphiques.

Vous savez que l'affectation d'une valeur à une variable est une simple copie d'information dans la mémoire centrale. Elle ne peut donc avoir aucun effet sur l'écran. Comment expliquer alors :

- que l'affectation d'une valeur à l'attribut Text d'une zone de texte provoque immédiatement l'affichage de cette valeur ?

- comment la modification des propriétés height ou width d'un composant modifie immédiatement ses dimensions à l'écran ?

Mais, tout d'abord, à quoi servent les propriétés ?

Utilité des propriétés

Pour répondre à cette question, prenons un exemple.

Imaginons que vous souhaitez représenter un article de magasin par une classe. Chaque article possède un prix hors taxe et un prix taxe comprise (TTC).

Si vous définissez le prix hors taxe et le prix TTC en tant que propriétés, vous pouvez par exemple vous arranger pour que le prix TTC soit automatiquement recalculé dès que le prix hors taxe est modifié.

Pour l'utilisateur de votre classe, cela sera totalement transparent. C'est-à-dire qu'il ne verra pas la différence avec l'utilisation d'un attribut.

Supposons, par exemple, que le prix hors taxe soit représenté par la propriété HT et le prix TTC, par la propriété TTC. Pour affecter la valeur 100 au prix hors taxe d'un article a, l'utilisateur de votre classe écrira :

a.HT := 100;

Et, sans qu'il le sache, cette « affectation » (en réalité, ce n'est pas une simple affectation, nous reviendrons là-dessus plus loin) aura en fait provoqué le calcul du prix TTC de cet article.

Supposons, par exemple, une taxe de 20% sur cet article. S'il affiche la propriété TTC (qui s'écrit donc a.TTC comme s'il s'agissait d'un attribut), l'utilisateur constatera que sa valeur est à présent 120, alors que nulle part dans le code qu'il a écrit ne figure une instruction modifiant cette valeur !

Cet exemple illustre un des aspects utiles des propriétés : le fait de pouvoir rendre des attributs interdépendants. Mais, il y en a d'autres :

- avec les propriétés, il est également possible de contrôler les données enregistrables dans un objet. On pourrait, par exemple, interdire l'affectation d'une valeur négative au prix hors taxe d'un article ;

- il existe un type particulier de propriétés, appelé propriétés tableaux, qui est très intéressant pour représenter des ensembles d'objets. Cet aspect est très utile pour la représentation de relations entre objets.

Déclaration des propriétés

Voyons à présent comment déclarer une propriété à l'intérieur d'une classe. Nous allons distinguer deux catégories de propriétés : les propriétés simples et les propriétés tableaux.

Déclaration des propriétés simples

La déclaration d'une propriété simple définit son nom, son type ainsi que le nom d'un accesseur en lecture et, optionnellement, celui d'un accesseur en écriture.

Si l'accesseur en écriture n'est pas donné, la propriété sera en lecture seule (l'affectation d'une valeur à la propriété provoquera une erreur de compilation).

Une propriété avec accesseur en écriture se déclare comme suit :

property nomP : typeP read nomAL write nomAE;

où nompP, typeP, nomAL et nomAE désignent respectivement le nom de la propriété, son type, le nom de l'accesseur en lecture et le nom de l'accesseur en écriture.

Pour déclarer une propriété en lecture, on ommet simplement la déclaration de l'accesseur en écriture comme ceci :

property nomP : typeP read nomAL;

L'accesseur en écriture est une méthode qui sera automatiquement appelée chaque fois qu'une expression quelconque sera affectée à la propriété. Il est défini comme une procédure avec un en-tête de la forme suivante :

procedure nomAE (v: typeP);

La valeur de l'expression affectée sera automatiquement transmise au paramètre v lors de l'appel.

L'accesseur en lecture, quant à lui, est une fonction sans paramètre retournant une valeur de même type que la propriété :

function nomAL () : typeP;

Cette fonction sera exécutée chaque fois que la propriété sera utilisée. C'est-à-dire que chaque fois que la propriété apparaît dans une expression, il faut imaginer que l'appel de l'accesseur en lecture figure à la place.

Notez bien qu'une propriété ne permet de stocker aucune valeur dans un objet. Pour cela, il faut prévoir un attribut (en accès privé de préférence, sinon cela n'a aucun intérêt !). L'accesseur en écriture ira a priori modifier cet attribut avec la valeur qui lui sera donnée et l'accesseur en lecture retournera simplement sa valeur. Mais cela n'est que théorique car les accesseurs sont des méthodes quelconques. Elles sont donc libres de faire ce qu'elles veulent !

Un exemple de classe utilisant des propriétés simples est donné plus bas.

Déclaration des propriétés tableaux

La déclaration des propriétés tableaux suit à peu de chose près le même principe, à part qu'il faut à présent ajouter des informations sur la manière d'indicer les éléments.

Contrairement aux tableaux classiques, les indices des propriétés tableaux peuvent être de type quelconque. On ne précise donc pas un indice de début et un indice de fin, mais simplement le type des indices :

property nomP [TypeI]: typeV  read nomAL write nomAE;

TypeI représente ici le type des indices et typeV le type des éléments.

L'accesseur en lecture possède à présent un paramètre qui définit l'indice de l'élément auquel on veut accéder :

function nomAL (i : TypeI) : typeV;

De même pour l' accesseur en écriture :

procedure  nomAE (i : TypeI; V : typeV);

Depuis l'extérieur de la classe, l'élément d'indice i d'un objet o est désigné par o.nomP[i], comme si la propriété nomP était un attribut de type tableau.

Comme tout est géré par les accesseurs, les données de ce « tableau virtuel » peuvent être en réalité stockées dans tout autre chose qu'un tableau. Par exemple, dans une liste (voir le cours sur les types structurés).

D'autre part, les propriétés tableaux ont la particularité de pouvoir être implicites. C'est-à-dire que l'on pourra écrire o[i] au lieu de o.nomP[i].

Il suffit pour cela de déclarer la propriété nomP comme étant la propriété par défaut de la classe en ajoutant le mot clé default à la fin de la déclaration :

property nomP [TypeI]: typeV  read nomAL write nomAE; default

Bien entendu, il ne peut y avoir qu'une seule propriété par défaut dans une classe donnée.

Je pense que tout cela sera plus clair avec un exemple. En voici un.

Exemple de propriété simple

Voici l'interface de la classe représentant un article de magasin :

Article = class
Strict private
A_HT: Double;
A_TTC: Double;
Function getHT (): Double;
Procedure setHT (c:Integer);
Function getTTC (): Double;

public
property HT : Double read getHT write setHT ;
property TTC : Double read getTTC;
end;

Les attributs A_HT et A_TTC servent respectivement à mémoriser les prix hors taxe et TTC de l'article. Mais ils sont privés et donc totalement inaccessibles depuis l'extérieur de la classe.

Nous avons également rendu les accesseurs privés, car l'accès public aux attributs se fait à présent par les propriétés HT et TTC.

Par contre, nous avons déclaré la propriété TTC en lecture seule. L'utilisateur de la classe ne pourra donc que modifier le prix hors taxe.

Et voici l'implémentation des accesseurs :

Function Article.getHT ():Double;
begin
getHT := A_HT;
end;

Procedure Article.setHT (p:Double);
begin
A_HT := p; A_TTC := p * 1.2;
end;

Function Voiture.getTTC (): Double;
begin
getTTC := A_TTC;
end;

Notez que l'accesseur en écriture de la propriété HT fait plus qu'on ne lui demande : il calcule immédiatement le prix TTC (le prix hors taxe augmenté de 20%) et l'affecte à l'attribut A_TTC. C'est ainsi que toute modification du prix hors taxe d'un article sera automatiquement répercutée sur son prix TTC.

Exemple de propriété tableau

Nous allons prendre comme exemple une classe TStock représentant le stock d'un magasin. La propriété En_Stock de cette classe nous permettra d'accéder au contenu du stock.

Déclaration et utilisation de la propriété En_Stock

Supposons, par exemple, que s soit un objet de la classe TStock ; alors s.En_Stock['Samsung Q45'] nous dira combien il reste de « Samsung Q45 » dans le stock s. Il s'agit donc d'une propriété tableau indicée par des chaînes de caractères (le libellé d'un article donné) et dont les éléments sont des entiers (quantité de chaque article).

Voici la déclaration de la propriété En_Stock :

property En_Stock [Libelle : String] : integer
read GetEnStock write SetEnStock; default;

Nous l'avons définie comme propriété par défaut, ce qui nous permet le raccourci d'écriture s['Samsung Q45'], au lieu de s.En_Stock['Samsung Q45'].

Les données de la propriété En_Stock sont mémorisées dans deux attributs privés : LesLibelles et LesQuantites. Le premier contient les noms des articles présents dans le stock et est déclaré comme suit :

LesLibelles : array [1 .. MAX_ARTICLE] of String;

Le deuxième contient la quantité de chaque article. C'est donc un tableau d'entiers. Nous le déclarons de la manière suivante :

LesQuantites : array [1 .. MAX_ARTICLE] of integer;

Et nous prenons les conventions suivantes :

- les valeurs du tableau LesLibelles sont toutes distinctes. Autrement dit, un même article ne peut pas se retrouver plusieurs fois dans ce tableau ;

- la quantité d'un article donné est définie par LesQuantites[i], où i est l'indice de son libellé dans le tableau LesLibelles. Par exemple, si LesLibelles[3] contient « Samsung Q45 », la quantité de Samsung Q45 dans le stock sera donné par LesQuantites[3] ;

- un article dont le libellé figure dans le tableau LesLibelles existe dans le stock. Il y en a donc nécessairement une quantité non nulle. Cela signifie également que le tableau LesQuantites ne contiendra toujours que des nombres strictement positifs.

Grâce aux propriétés, tout ceci sera totalement transparent à l'utilisateur de la classe, qui pourra facilement ajouter ou retirer des articles du stock en ignorant totalement le travail effectué en arrière plan par les accesseurs.

Par exemple, pour ajouter dix Samsung Q45 :

s['Samsung Q45'] := s['Samsung Q45'] + 10;

Pour en retirer 15 :

s['Samsung Q45'] := s['Samsung Q45'] - 15;

Et cette opération sera sans effet, s'il n'en restait par exemple que 12.

Pour retirer tous les Samsung Q45 du stock :

s['Samsung Q45'] := 0;

Pour ajouter un nouvel article, on lui affecte une quantité strictement positive. Par exemple, l'instruction

s['Clé USB Sony 8GB'] := 20;

sert à priori à placer vingt « clés USB Sony 8GB » dans le stock. Si, par hasard, cet article est déjà en stock, l'ancienne quantité sera effacée.

Interface de la classe Stock

Voici l'interface de la classe Stock :

TStock = class
strict private

NArticle :  integer;
LesLibelles : array [1 .. MAX_ARTICLE] of String;
LesQuantites : array [1 .. MAX_ARTICLE] of integer;
Curseur : integer;
function Indice (Libelle : String) : integer;
function GetEnStock (Libelle: String) : integer;
procedure SetEnStock (Libelle : String; n : integer);
procedure Supprimer (Libelle : String);

public
property En_Stock [Libelle : String] : integer
read GetEnStock write SetEnStock; default;
constructor Nouveau ();
function ArticleSuivant (): String;
procedure Debut ();
end;

NArticle contient l'indice du dernier article. Un stock vide se reconnaîtra donc par NArticle = 0 et un stock plein, par NArticle = MAX_ARTICLE.

L'attribut Curseur, ainsi que les méthodes ArticleSuivant et Debut, permettent de parcourir les articles d'un stock avec une boucle. Nous en reparlerons plus loin.

La fonction Indice retourne l'indice, dans le tableau LesLibelles, d'un article de libellé donné. Si cet article est absent, elle retourne la valeur 0.

La procédure Supprimer supprime un article de libellé donné en décalant les éléments suivants vers la gauche (voir le cours sur les tableaux, gestion des tableaux remplis partiellement).

Implémentation des accesseurs de la méthode En_Stock

Nous n'allons pas détailler ici l'implémentation de toutes les méthodes de la classe (si cela vous intéresse, consultez le code source du projet Magasin dans le répertoire Exemple-ProgObjet2), mais uniquement les accesseurs de la propriété tableau En_Stock.

Voici l'accesseur en lecture :

function TStock.GetEnStock (Libelle: String) : integer;
var i : integer;
begin
i := Indice (Libelle);
If i = 0 then GetEnStock := 0
else
GetEnStock := LesQuantites[i];
end;

Et voici l'accesseur en écriture :

procedure TStock.SetEnStock (Libelle : String; n : integer);
var position : integer;
begin
position := Indice (Libelle);
If position = 0 then
begin
If (NArticle <> MAX_ARTICLE) and (n > 0) then
begin
NArticle := NArticle  + 1;
LesLibelles [NArticle]:=Libelle;
LesQuantites [NArticle] := n;
end
end
else
if n >= 0 then
if n = 0 then Supprimer (Libelle)
else LesQuantites [position] := n;

end;

La logique de cette procédure est la suivante :

- Nous commençons par repérer la position de l'article.

- Si l'article est introuvable (position = 0), on l'ajoute au stock. Mais cela n'est possible que si la quantité à ajouter est un nombre strictement positif (n > 0) et que le stock n'est pas rempli (NArticle <> MAX_ARTICLE). Si ces deux conditions sont vérifiées, on ajoute l'article dans le stock avec la quantité n associée. Ceci est réalisé en empilant le libellé dans le tableau LesLibelles et la quantité dans le tableau LesQuantites (pour la notion de pile, voir également le cours sur les tableaux, gestion des tableaux remplis partiellement).

- Si l'article existe, il y a trois possibilités :

- n est négatif : on ne fait rien ;

- n = 0 : on supprime l'article ;

- N > 0 : on affecte la quantite n à cet article.

Utilisation de la classe Stock

Dans le dossier Exemple-progObjet2/Magasin, vous trouverez un exemple de projet utilisant la classe stock. Voici son interface graphique :

Voici la procédure évènementielle associée au bouton Commander :

procedure TForm1.BT_CommanderClick(Sender: TObject);
var libelle: string; quantite : integer;
begin
libelle := ZT_LIB.Text;
quantite := StrToInt(ZT_Quantite.Text);
Stock[libelle]:= Stock[libelle]+quantite;
AfficherLeStock ();
end;

Commander un article consiste donc à ajouter la quantité commandée au stock. Le nouveau contenu du stock est affiché à l'écran par la procédure AfficherLeStock, dont nous reparlerons plus loin.

Celle du bouton Vendre suit le même principe. On remplace simplement le + par un - :

procedure TForm1.BT_VendreClick(Sender: TObject);
var libelle: string; quantite : integer;
begin
libelle := ZT_LIB.Text;
quantite := StrToInt(ZT_Quantite.Text);
Stock[libelle]:= Stock[libelle]-quantite;
AfficherLeStock ();
end;

Affichage du stock

La procédure AfficherLeStock affiche le contenu du stock dans la zone de liste. Elle n'utilise pas les propriétés tableaux et n'est donc pas essentielle pour la compréhension de ce cours. Elle vous servira néanmoins à comprendre l'intérêt de l'attribut Curseur et des méthodes ArticleSuivant et Debut.

La méthode ArticleSuivant permet de parcourir le stock avec une boucle, sans connaître le nombre d'articles (rappelez-vous que NArticle est un attribut privé).

Nous utilisons ici le principe du curseur pour parcourir séquentiellement un ensemble de données (on retrouve par exemple ce principe dans le parcours séquentiel d'un fichier ou dans la lecture d'une table d'une base de données).

Dans notre cas, le curseur est un nombre entier qui définit l'indice du dernier article lu. Il est représenté par l'attribut privé Curseur de la classe Stock.

Comme il s'agit d'un attribut privé, l'utilisateur de la classe ne peut pas agir directement sur lui. Par contre, il dispose de la méthode publique ArticleSuivant, qui lui permet d'avancer le curseur d'un cran. Il s'agit plus précisément d'une fonction qui retourne le libellé de l'article suivant. Voici le code de cette méthode :

function TStock.ArticleSuivant (): String;
begin
if Curseur = NArticle then ArticleSuivant := 'FIN'
else
begin
Curseur := Curseur + 1;
ArticleSuivant := LesLibelles[Curseur];
end;
end;

Si le curseur pointe sur le dernier article (Curseur = NArticle), cela signifie que l'on a parcouru tout le stock. Dans ce cas, la fonction n'avance pas le curseur et retourne simplement la chaîne 'FIN' pour signifier que l'on est arrivé au bout du stock.

Par contre, s'il ne pointe pas sur le dernier article, le curseur est incrémenté et la fonction retourne le libellé de l'article sur lequel il pointe après l'incrémentation. C'est-à-dire le libellé de l'article suivant.

La méthode Debut permet de positionner le curseur au début du stock :

procedure TStock.Debut ();
begin
Curseur := 0;
end;

Elle doit donc être appelée avant de parcourir le stock avec une boucle.

À présent, voyons le code de la procédure AfficherLeStock :

procedure TForm1.AfficherLeStock ();
var libelle : String; quantite: integer;
begin
Stock.Debut; ZL_Stock.Clear;
libelle := Stock.ArticleSuivant ;
While libelle <> 'FIN' do
begin
ZL_Stock.Items.Add(libelle+': '
+IntToStr(Stock[libelle]));
libelle := Stock.ArticleSuivant;
end;
end;

Tant que l'on n'est pas à la fin du stock (libelle <> 'FIN'), on affiche l'article courant puis on passe à l'article suivant.

Si le stock n'est pas vide, les instructions Stock.Debut et Stock.ArticleSuivant figurant avant la boucle placent le curseur sur le premier élément.

S'il est vide, libelle contiendra la chaine 'FIN' dès le départ et la boucle d'affichage ne sera pas exécutée.

Propriétés graphiques

Revenons à présent sur les composants graphiques et leurs « attributs étranges ». Tout peut à présent s'expliquer grâce aux propriétés. Par exemple, pour une zone de texte ZT, l'écriture ZT.Text, ne représente pas un attribut, mais une propriété. Cela explique comment une affectation comme ZT.Text := 'Truc' provoque l'affichage de « Truc » à l'écran : c'est l'accesseur en écriture de la propriété ZT qui fait ceci !

En fait, lors de la compilation, l'instruction ZT.Text := 'Truc' est traduite en un appel de l'accesseur en écriture de ZT et non pas en une affectation.

On peut expliquer de la même manière comment la modification de la propriété width d'un contrôle va immédiatement modifier sa largeur à l'écran. Ainsi, toutes les affectations à des « attributs » de composant qui provoquent des affichages sont en fait des affectations à des propriétés.

Héritage, surcharge et polymorphisme

La notion d'héritage a déjà été abordée dans le cours I sur la programmation objet, mais simplement du point de vue de l'utilisation.

Dans cette partie du cours, nous allons voir comment définir explicitement une relation d'héritage entre deux classes, puis comment exploiter de la meilleure manière cette relation par la surcharge et le polymorphisme.

Présentation de l'exemple

L'exemple de projet qui nous servira à introduire ces notions se trouve dans le dossier Exemple-ProgObjet2/Formes.

Il contient la définition d'une classe Cercle comme sous-classe d'une autre classe nommée Forme.

Toute forme possède une aire. Un cercle est une forme spéciale qui possède également un rayon.

Voici l'interface graphique de ce projet :

Le bouton Nouvelle Forme permet de générer une forme d'aire aléatoire.

Le bouton Nouveau Cercle permet de générer un cercle de rayon aléatoire.

Chaque forme générée (cercle ou forme quelconque) est stockée dans un même tableau TF, dont les éléments sont de type Forme. Le nombre de formes générées est stocké dans un entier NF. TF et NF sont des variables globales dont voici la déclaration :

Notez qu'il est impossible, en principe, de stocker dans un même tableau des données de types différents. Nous verrons un peu plus loin ce qui permet ce miracle.

Le bouton Afficher permet d'afficher les attributs d'une forme donnée en donnant son numéro (l'indice de l'élément du tableau dans lequel elle est rangée).

La figure précédente montre l'affichage de la forme numéro 3 : c'est une instance de la classe Forme dont l'aire est égale à 71.

Voici l'affichage de la forme numéro 2 :

Il s'agit cette fois-ci d'un cercle.

Interfaces des deux classes

Voici les interfaces des deux classes. :

La classe Forme possède deux méthodes :

- Create est le constructeur de la classe. Il permet de créer une forme d'aire donnée (paramètre A) ;

- AjouterForme range une forme dans le tableau TF, incrémente le nombre de formes NF et affiche sa valeur ;

- AfficherAttributs permet d'afficher les attributs d'une forme : son aire et sa classe (Forme).

La classe Cercle est définie comme une sous-classe de la classe Forme. Elle possède trois méthodes :

- Create est le constructeur de la classe. Il permet de créer un cercle de rayon donné (paramètre R) ;

- AfficherAttributs permet d'afficher les attributs d'un cercle : son aire, sa classe (Cercle) et son rayon ;

- CalculAire permet de calculer l'aire d'un cercle.

Implémentation de la classe Forme

Voici l'implémentation de la classe Forme :

Dans le corps de la méthode AjouterForme apparaît un mot-clé self.

Ce mot-clé peut être utilisé dans toute méthode pour désigner l'instance à laquelle la méthode s'applique. Il représente plus précisément son adresse.

Ici, self représente l'adresse de la forme à laquelle on applique la méthode AjouterForme.

L'affectation TF[NF]:=Self ajoute donc la forme (en fait son adresse) dans le tableau TF, à la suite des formes déjà mémorisées.

Dans la méthode AfficherAttributs figure l'appel de la méthode ClasseName. Cette méthode retourne le nom de la classe à laquelle appartient un objet quelconque. C'est en fait une méthode de la classe TObject dont, rappelons-le, toute classe Pascal est une sous-classe. Elle peut donc être appliquée à une instance de la classe Forme.

Implémentation de la classe Cercle

Voici l'implémentation de la classe cercle :

L'affichage des attributs d'un cercle est assez similaire à l'affichage des attributs d'une forme. Il suffit de rajouter l'affichage du rayon.

Pour éviter d'écrire deux fois le même code, nous avons utilisé ici la surcharge.

Dans le corps de la procédure AfficherAttributs figure l'instruction inherited AfficherAttributs. Le mot-clé inherited permet de préciser qu'il s'agit de la méthode AfficherAttributs de la classe-mère.

En appelant cette méthode, on obtient donc l'affichage de l'aire du cercle et de sa classe.

De manière générale, une méthode est surchargée si elle appelle une méthode de même nom de la classe-mère. C'est un mécanisme qui permet de spécialiser une méthode sans écrire deux fois le même code.

Utilisation des deux classes

Voici les procédures évènementielles associées aux boutons de l'interface graphique :

Affichage d'une forme

L'affichage d'une forme donnée est réalisée par la procédure Bt_AfficherClick.

Elle commence par lire le numéro (num) de la forme à afficher, c'est-à-dire l'indice de l'élément du tableau TF dans lequel cette forme est stockée.

TF[num] contient donc un objet de la classe Forme ou un objet de la classe Cercle.

Pour distinguer les deux cas, nous avons utilisé l'opérateur is, qui permet de savoir si un objet appartient à une classe donnée.

Si un objet appartient à une classe, il appartient également à sa classe-mère !

Si TF[num] est une instance de la classe Cercle, nous ne pouvons pas appliquer directement la méthode AfficherAttributs pour l'afficher.

En effet, bien que TF[num] contienne un objet de la classe Cercle, il est considéré par le compilateur comme un objet de la classe Forme car le tableau TF a été déclaré comme un tableau de Forme.

Pour forcer le compilateur à interpréter TF[num] comme un objet de la classe Cercle, nous avons utilisé le polymorphisme, qui se traduit en Pascal par l'opérateur as.

TF[num] as Cercle signifie que le compilateur doit interpréter TF[num] comme un cercle. De ce fait, la méthode AfficherAttributs appliquée à cette objet sera bien celle de la classe Cercle.

De manière générale, le polymorphisme est le fait de pouvoir forcer le compilateur à interpréter un objet d'une certaine classe (à la déclaration) comme un objet d'une classe-fille de cette classe, de manière à pouvoir lui appliquer les méthodes de la classe-fille.

Création d'un cercle

La génération aléatoire d'un cercle et son stockage dans le tableau TF est réalisée par la procédure Bt_NouveauCercleClick.

Elle illustre un autre aspect du polymorphisme. En effet, lorsque l'on applique la méthode AjouterForme au cercle généré, cela provoque (voir le code de la méthode AjouterForme) son stockage dans un élément du tableau TF.

Or ces éléments sont a priori tous des objets de la classe Forme !

En fait, cela ne pose pas de problème au compilateur car TF ne contient que des pointeurs.

L'instruction TF[NF]:=self ne fait rien d'autre que stocker l'adresse de l'objet dans le tableau TF.

Exercices et exemples

Téléchargez les exercices :

- Programmation objet - Cours II.

Pour obtenir les corrigés ainsi que les exemples, le téléchargement est possible via un login et un mot de passe, que vous pouvez obtenir en envoyant un mail à l'adresse suivante :

en précisant un peu qui vous êtes et les raisons pour lesquelles ce cours vous intéresse.