Les propriétés


En plus des attributs et des méthodes, Pascal Objet offre la possiblité 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

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 afin 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 par la différence avec l'utilisation d'un attribut normal.

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

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éclarations 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 optionellement, 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;

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 est affectée à la propriété. Il est défini comme une procédure avec une entê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 quand à 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é est utilisée. Cet à dire que chaque fois que la propriété apparait 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 à priori modifier cet attribut avec la valeur qui lui sera donné 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é ici.

Déclarations 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éfini 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 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 inacessibles depuis l'extérieur de la classe.

Nous avons également rendu les accesseurs privés, car l'accès publique 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'implementation 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 Article.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 calcul 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 chaines de caractères (le libellé d'un article donnée) 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:

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é USB Sony 8GB' dans le stock. Si par hasard, cet article était 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 reconnaitra 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 procedure Supprimer, supprime un article de libellé donné en décalant les éléments suivants vers la gauche (voir cours sur les tableaux, gestion des tableaux remplis partiellement.)

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

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:

Magasin-Presentation-Interface.jpg, 22kB

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éret de l'attribut Curseur et des méthodes ArticleSuivant et Debut.

La méthode ArticleSuivant permet de parcourir le stock avec une boucle, sans connaitre 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 cet attribut. 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écisemment 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 chaine '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.

A 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 qu'on 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.