Recherche
1 connecté

  Actuellement : 2 commentaires


La programmation orientée objet en Delphi - Un guide pour les débutants

Un article de : Rick Spence | Traduit de l'anglais par : DarkSide

Le Pascal Objet de Delphi, est un langage orienté objet. Cela signifie simplement que ce langage permet au programmeur de créer et de manipuler des objets. De manière plus détaillée disons que ce langage met en oeuvre les quatre principes de la programmation orientée objet :

  • Abstraction des données,
  • Encapsulation,
  • Héritage,
  • Polymorphisme.
Comme vous allez le voir, ce sont des mots bien compliqués pour, en fait, exprimer des idées très simples. Pour avoir formé des centaines de programmeurs Delphi, j'ai trouvé que de bien maitriser la programmation objet que propose Delphi est la différence entre juste utiliser Delphi et réellement tirer le maximum de ce produit.

Dans cet article je vais initier les programmeurs Delphi aux possibilités de l'orientation objet du Pascal objet et montrer comment tirer partie d'elles dans vos propres applications. Même si vous avez déjà utilisé Delphi, vous trouverez dans cet article un sujet de révision utile - c'est impressionnant de voir tout ce qu'on peut faire avec Delphi sans réellement comprendre les principes du langage.

Cet article commence avec quelques définitions toutes simples. Il commence par décrire la programmation orientée objet en général, puis définit précisément deux termes que vous avez sans doute déjà entendus - objet et classe. Il abordera ensuite les mécanismes du langage que nous utilisons pour travailler avec les objets, et montrera comment s'assurer que vos objets seront correctement libérés (que votre programme n'a aucune fuite de ressources). Ensuite, l'article traite en détail de la syntaxe utilisée pour créer des classes ; vous verrez une véritable classe que vous pourrez utiliser directement dans vos programmes.

Vous avez dit POO ? Qu'est-ce qu'un objet ? Quid des classes ?


La programmation orientée objet (POO en abbrégé), est tout ce qui concerne l'écriture des programmes qui manipulent des objets. Delphi, tout comme C++ et Java, est un vrai langage orienté objet. Comme vous le verrez les principes de la programmation orientée objet sont les mêmes dans tout ces langages avec bien sûr une syntaxe différente. une fois que vous en aurez étudié les principes et, quelque soit le langage que vous avez employé pour l'apprendre, vous verrez que ces connaissances se tranfèrent facilement aux autres langages. Les concepts tels que héritage ou abstraction de donnée sont les mêmes en C++, Java et Delphi - seule la syntaxe diffère.

Que vous ayez déjà utilisé un langage orienté objet ou non, vous avez probablement entendu les termes "objet" ou "classe" ici ou là. Une classe est une construction de programmation que les développeurs utilisent pour spécifier et mettre en oeuvre de nouveaux types de données tels que des entiers, des chaines de caractères, etc. Les langages orientés objet permettent aux programmeurs de créer leurs propres types de données tels que étudiants, comptes et menus. Puisque ces types de données ne sont pas inclus dans le langages ni dans le hard de l'ordinateur, nous les appelerons types de données abstraits.

Le mécanisme de langage que les développeurs utilisent pour ce faire est appelé classe. Une classe est donc la spécification et l'implémentation d'un type de données abstrait. Si vous n'avez jamais utilisé un langage orienté objet avant, ce concept sera nouveau pour vous. Voyez le comme ça. Quel que soit le langage que vous ayez utilisé avant, vous avez pris l'habitude d'utiliser ces types de données - chaines de caractères, entiers, réels, booléens, etc. Une classe permet aux programmeurs de créer leurs propres types de données lesquels peuvent être utilisés comme les types prédéfinis du langage. La VCL (Visual Component Library) de Delphi est simplement une collection de classes écrite par l'équipe Borland / Imprise. Toutes ces choses que vous avez sans doute déjà utilisé dans vos applications telles que : boutons radio, cases à cocher, images, mémos, étiquettes, etc. sont toutes des classes définies dans la VCL.

Vous verrez que vous pouvez créer vos propres classes et les utiliser de la même manière. Plus loin dans cet article nous nous occuperons en détail de la syntaxe. Pour l'instant considérons cette déclaration de classe :

Type
  TEtudiant = Class
    FNom: Integer;
    FPrenom : Integer;
  FTel : String;
End;

Cette déclaration declare un nouveau type de données appelé TEtudiant. Le type de données TEtudiant est représenté par 3 informations : Le nom (FNom), le prénom (FPrenom) et le numéro de téléphone (FTel). Votre programme maintenant peut procéder à la déclaration des variables de type TEtudiant :

Var
  Etudiant1 : TEtudiant;
  Etudiant2 : TEtudiant;

La seule difference entre ceci et déclarer des variables de types déclarés dans la VCL tel que :

Var
  StringList1 : TStringList;
  IniFile1 : TIniFile;

est que dans la première vous avez déclaré votre propre type (TEtudiant) tandis qu'avec la dernière la VCL a déclaré les types de données (TStringList and TIniFile).

Pour permettre au compilateur de trouver la déclaration de classe que vous utilisez, votre programme doit explicitement utiliser l'unité déclarant les classes. Si vous avez créé un programme comprennant une fiche, Delphi va automatiquement générer pour vous une clause USES qui va lister les unités de la VCL les plus communément utilisées. C'est de cette manière que votre programme aux classes des fiches, cases à cocher et boutons sans lister explicitement les unités qui les contiennent. Si vous ne créez pas de fiche (si vous avez une unité vide) Delphin ne génèrera pas cette clause USES et si vous tentez de faire référence à quelque classe de la VCL que ce soit vous obtiendrez des erreurs de compilation. La même règle s'applique à vos propres classes. Si une unité de votre application fait référence à une classe d'une autre unité, la première unité doit lister la seconde unité dans sa clause USES.

Nous avons parlé de classes, mais que sont les objets ? C'est facile. Un objet n'est ni plus ni moins qu'une instance d'une classe - une variable dont le type de données est une classe. Etudiant1 est un objet. Etudiant2 un autre. Tout comme StringList1 et IniFile1. Le terme objet, donc, est un terme utilisé pour décrire toute variable dont le type est une classe. Les objets peuvent bien sûr être de n'importe quelle classe, donc pour décrire un objet on utilise aussi le nom de sa classe - nous parlerons donc d'objet bouton, d'objet fiche, d'objet Etudiant, etc.

Travailler avec des objets

Avant de commecer à écrire nos propres classes, faisons une rapide révision sur la manière de travailler avec des classes et des objets. Nous utiliserons la classe TStringList de Delphi (déclarée dans la VCL) pour illustrer ces points. La première étape est de déclarer une vaiable de type TStringList.

Var
  StringList1 : TStringList; // TStringList est la classe, StringList1 l'objet.

Où placer la déclaration ? Ca dépend de où vous voulez utiliser cet objet (sa portée), et combien de temps vous voulez l'utiliser (sa durée de vie). Si vous voulez seulement l'utiliser dans une sous-routine alors déclarez-le dans cette sous-routine :

Procedure Test;

Var
  StringList1 : StringList;

Begin
  // Traitement avec StringList1 ici ...

End;

Dans ce cas, aussitôt la procédure Test terminée, vous ne pouvez plus utiliser cet objet ; vous pouver seulement y accéder dans cette sous-routine. Si vous voulez qu'un objet ait une portée plus grande et une plus longue durée de vie, vous devez le déclarer hors de la sous-routine. Si vous le placez dans la section implementation de l'unité, hors de toute sous-routine, l'objet est visible dans toute l'unité et dans toute unité utilisant cette unité.

Maintenant, indépendamment de l'endroit où vous déclarez l'objet, votre programme est reponsable de l'allocation et de la libération de la mémoire qu'il occupe. C'est la différence principale entre le travail avec des objets et des variables simples. Quand vous travaillez avec des variables simples vous pouvez les utiliser directement après les avoir déclarées :

Var
  i : Integer;

Begin
  i := 10;

Simplement en déclarant la variable, de la mémoire lui est allouée. En revanche, en travaillant sur un objet vous devez tout d'abord lui allouer sa mémoire :

Var
  Etudiant1 : TEtudiant;

Begin
  // Allocation de la memoire à l'objet

Pour allouer la mémoire vous devez appeler une routine spéciale appelée constructor. Un contructeur de classe peut porter n'importe quel nom, ainsi, les classes peuvent avoir plusieurs constructeurs. Nonobstant, la plupart des classes déclarent un constructeur appelé Create. Vous apprendrez comment écrire vos propres constructeurs plus loin dans cet article quand vous apprendrez comment écrire  vos propres classes. Pour l'instant nous utiliserons des classes prédéfinies, nous nous ocuperons donc seulement de leurs constructeurs. Le constructeur de la classe TStringList est appelé Create. Pour appeler le constructeur vous devez préfixer le constructreur avec le nom de la classe :

TEtudiant1.Create;

Le constructeur est une fonction qui renvoie un pointeur sur la mémoire qu'il a alloué. Donc, pour allouer de la mémoire à l'objet, voua appelez Create, en utilisant la syntaxe suivante : 

Var
   Etudiant1 : TEtudiant;
Begin
   // Alloue de la memoire à l'objet – noter la forme générale :
   // <Objet> := <NomClasse>.<NomConstructeur>;
   Etudiant1 := TEtudiant.Create;

Ceci s'appelle instancier la classe.

La plus grande erreur que font les gens débutant avec les objets Delphi est d'oublier d'appeler son construteur, ou de l'appeler avec la mauvaise syntaxe.

Une fois la mémoire allouée pour la classe vous pouvez accéder à ses données en utilisant l'opérateur point ('.') :

Etudiant1 := TEtudiant.Create;
Etudiant1.FNom := 'Spence';
Etudiant1.FPrenom := 'Rick';

Si vous essayez d'accéder aux données sans, d'abord, allouer de la mémoire (oublier d'instancier la classe) vous obtiendrez des erreurs à l'exécution du programme.

Il est bien sûr possible de travailler avec des objets multiples :

Var
   Etudiant1 : TEtudiant;
   Etudiant2 : TEtudiant;
Begin
   // Alloue de la memoire aux objets
   Etudiant1 := TEtudiant.Create;
   Etudiant2 := TEtudiant.Create;

et chaque objet a sa propre mémoire. Dans cet exemple Etudiant1 est associé à trois éléments de données, ainsi que Etudiant2 :

Etudiant1 := TEtudiant.Create;
Etudiant2 := TEtudiant.Create;

Etudiant1.FNom := ‘Spence’;
Etudiant2.FNom := ‘Brown’;

Maintenant, vous avez la responsabilé de libérer la mémoire de l'objet. Pour se faire vous devez appeler une autre routine appelée free. Pour cela, vous devez préfixer free avec le nom de l'objet :

Etudiant1.Free;

Vous noterez que la syntaxe n'est pas symétrique. On préfixe le nom du constructeur avec le nom de la classe mais on préfixe free avec le nom de l'objet. Dans la suite vous verrez que cette différence permet à un code d'affecter un objet (méthode normale), et d'appeler un code pour affecter une classe (méthode de classe).

Voici une routine complète permettant de déclarer, instancier et libérer un objet TStringList

Procedure Test;

Var
  StringList1 : StringList;

Begin
  // Appelle le constructeur pour allouer la mémoire
  StringList1 := TStringList1.Create;

  // Utilisation de stringlist1 ici ...

  // Libération de la mémoire ici
  StringList1.Free;

End;

Dans cet exemple nous avons alloué et libéré la mémoire dans la même routine. C'est bien dans ce cas puisque l'objet est visible dans la routine. Si l'objet avait été visible dans toute l'unité vous devez décider quand libérer sa mémoire. Il arrive souvent qu'un objet ait la même durée de vie que la fiche. Ceci étant, une fiche peut avoir besoir de d'utiliser une StringList, vous devez donc créer la StringList quand vous créez la fiche, et vous devez libérer la StringList quand la fiche est libérée. Pour se faire vous devez instancier la classe StringList (appeler son construteur) dans le create de la fiche, et libérer la StringList  (appeler free) dans l'évènement OnDestroy de la fiche.

Ceci fait apparaitre qu'il est de votre responsabilité d'allouer et de libérer la mémoire de l'objet - si vous oubliez de libérer la mémoire vous créez une fuite de ressource . Vous avez alloué de la mémoire mais ne l'avez jamais libérée. Serez-vous capable de vous en apprececoir ? Ca dépend de combien de mémoire l'objet nécessite et combien de fois vous instanciez cette classe. Si l'objet nécesite 2K de mémoire et que vous l'allouez chaque fois que l'utilisateur clique un bouton, votre application va rapidement se terminer sur une erreur de mémoire insuffisante et vous devrez redémarrer l'ordinateur. Par contre, si l'objet ne demande que quelques octets et que vous l'instanciez qu'une ou deux fois vous ne vous en appercevrez pas.

En résumé, donc, il est de votre responsabilité d'allouer et de libérer la mémoire de vos objets, et ce n'est pas aussi simple que vous pouvez le penser, comme le montre ce qui suit :

S'assurer que la mémoire de votre objet est libérée

Considérons le code suivant :

Procedure Test;
Var
   i, j : Integer;
   stringList1 : TStringList;

Begin
   stringList1 := TStringList.Create;
   i  := 10;
   j := 0;
   i := i div j; // Ligne 10 - Exception générée ici
   stringList1.Free; // Ligne 11 – Jamais executée
End;

Nous avons intentionnellement généré une erreur de division par zéro à la ligne 10. Si vous débugguez ce code, vous verrez que Delphi n'exécute jamais la ligne 11. Après que l'exeption soit détectée à la ligne 10, Delphi gère l'exeption puis retourne à la boucle évènementielle principale du programme. La mémoire de l'objet n'est pas libérée. Naturellement ceci est un exemple mais vous devez faire attention qu'une exeption n'empêche pas votre appel de libérer la mémoire. Borland recommende d'inclure les create et free dans une structure try / Finally. La structure try / Finally fait partie du Pascal standard, et voici comment ça marche. Vous utilisez try pour marquer le début d'un bloc de code. Vous utilisez Finally pour marquer le début d'un second bloc de code puis End pour indiquer la fin de la construction :

Try
  <Premier Bloc de code>
Finally
  <Second Bloc de code>
End;

Si une instruction dans le premier bloc de code génère une exception, Delphi exécutera le code du deuxième bloc avant de gérer l'exception. Si le premier bloc ne génèrepas d'exception le deuxieme bloc est quand même exécuté. Donc, ceci garanti l'exécution du deuxième bloc qu'une exception se produise ou non. Pour s'assurer que la mémoire de l'objet soit libérée, on place l'appel à Free dans la section Finally. Voici la forme générale :

<Objet> := <NomClass>.<NomConstructeur>
Try
   // Utilisation de l'objet ici
Finally
   <Objet>.Free;
End;

Noter que l'appel au constructeur précède le Try. C'est au cas où le constructeur lui-même échoue. Si le constructeur échoue, Delphi n'allouera pas de mémoire à l'objet. Si l'appel au constructeur avait été après le Try, Delphi aurait tenté d'exécuter le code du bloc Finally et aurait tenté de libérer la mémoire d'un objet n'en possédant pas. En plaçant l'appel au constructeur avant le Try, si le constructeur échoue le code du bloc Finally n'est pas exécuté.

Si vous devez allouer et libérer plusieurs objets, l'utilisation de Try/ Finally est un peu plus complexe. Voici un bout de code :

Var
Etudiant1 : TEtudiant;
StringListl : TStringList;

Begin
   // Allocation de mémoire aux objets
   StringListl := TStringList.Create;
   Etudiant1 := TEtudiant.Create;
   Try
      // Utilisation de Etudiant1 & StringList1
    Finally
      Etudiant1.Free;
      StringList.Free;
End;

Est-ce que ce code garanti que chaque objet soit libéré ? Non. Si le constructeur de TEtudiant échoue le programme ne rentrera pas dans le bloc Try, donc, le bloc Finally n'est pas appelé et la mémoire de TStringList1 n'est pas libérée. La solution est d'écrire deux blocs Try / Finally :

Var
  Etudiant1 : TEtudiant;
  StringListl : TStringList;

Begin
  // Allocation de mémoire aux objets
  StringListl := TStringList.Create;
  Try
    Etudiant1 := TEtudiant.Create;
  Try
  // Utilisation de Etudiant1 & StringList1
  Finally
    Etudiant1.Free;
  End;
  Finally
    StringList.Free;
End;

Ceci fonctionne, mais c'est lourd ; imaginez le code si vous avez 3 objets ou plus à créer. Une astuce que vous pouvez utiliser s'appuie sur le fait que Free ne libèrera pas un objet qui est nul (Nil). Le code source de Free est :

// Free de Delphi
If ObjectEnCoursDeLiberation <> Nil Then
   LibereLaMemoire;  // Self.Destroy

Avant de libérer la mémoire de l'objet, Free s'assure que que l'objet n'est pas nul. Cela veut-il dire que  le code suivant fonctionne ?

Var
  Etudiant1 : TEtudiant;
Begin
  // Oubli d'instanciation de TEtudiant
  Etudiant1.Free;

La réponde dépend de la valeur de Etudiant1 quand l'appel à Free est fait. Est-ce que Etudiant1 est nul ? Non. En Delphi les variables n'ont pas de valeur initiale - la valeur actuelle de Etudiant1 dépend ce qu'il y a dans la pile du processeur, à la position occupée par Etudiant1 quand la routine est appelée. Ce qui suit fonctionnera :

Var
   Etudiant1 : TEtudiant;
Begin
   Etudiant1 := Nil;

// Oubli d'instanciation de TEtudiant
   Etudiant1.Free;

Comme est-ce que ceci va nous aider à résoudre le problème dans l'allocation d'une série d'objets et garantir qu'ils seront libérés ? Bon, ça veut dire qu'on peut écrire ceci :

TEtudiant1 := Nil;
Try
  Etudiant1 := TEtudiant.Create;
Finally
  Etudiant1.Free;
End;

L'appel à Free n'échouera pas même si le constructeur échoue ; on a explicitement affecté la valeur Nil à Etudiant1 avant que le constructeur n'échoue. Le constructeur échoue (n'alloue pas de mémoire à Etudiant1) donc Etudiant1 conserve sa valeur à Nil et l'appel à Free néchoue pas.

Si nous étendons ça au problème d'allocation à plusieurs objets, nous pouvons écrire :

Var
  Etudiant1 : TEtudiant;
  StringListl : TStringList;

Begin
  Etudiant1 := Nil;
  StringList1 := Nil;
  Try
    StringListl := TStringList.Create;
    Etudiant1 := TEtudiant.Create;
    // Utilisation de Etudiant1 & StringList1
  Finally
    Etudiant1.Free;
    StringList.Free;
End;

L'ancienne solution – c.-à-d. l'emboitement des blocs Try / Finally est classiquement la meilleure solution, mais la derniere, - c.-à-d. mettre expicitement les objets à Nil  et compter sur Free pour ne pas détruire les objets qui sont à Nil est certainement la plus commode.

J'essaie de ne pas employer trop de produits tiers avec Delphi, mais il y a un type de produit add-on que je vous recommende chaudement d'utiliser (ceux qui vérifient vos programmes contre les fuites de ressouces). Il y a deux programmes qui entrent dans cette catégorie Memory Sleuth (Delphi 2, 3, 4 et 5), AQtime (Delphi 2, 3, 4, 5, 6, 7 et 8) et Numega bound checker. Voici comment ils fonctionnent. Ils surveillent l'utilisation des ressources allouées par votre programme  - dans ce cas-ci la ressource dont nous parlons est la mémoire, mais ils surveillent également d'autres ressources plus basses telles que des contextes de handles et des contextes de périphériques. Quand votre programme se termine, s'il n'a pas libéré toutes les ressources qu'il a assigné, ces produits donnent une liste de toutes ces attributions, y compris la ligne réelle dans le code source qui a alloué la ressource. Je vous recommande vivement d'utiliser un de ces produits - vous pourriez être étonnés de ce que vous trouverez…

Declarer vos propres classes

Comme vous le savez, une classe est une structure de programmation que vous utilisez pour spécifier et implémenter un type de données abstrait. La classe spécifie l'élément de données nécessaire pour stocker les données de l'objet. Précédemment nous avons déclaré notre classe TEtudiant telle que :

Type
  TEtudiant = Class
  FNom : Integer;
  FPrenom : Integer;
  FTel : String;
End;

Cette déclaration de classe définit les conditions de stockage pour l'objet TEtudiant. Si vous créez 4 objets TEtudiant, chacun aura les même propriétés. La structure de chaque objet TEtudiant est identique, et cette structure est déterminée par la déclaration de classe.

Ces élément de données utilisés pour représenter un objet - dans notre cas FNom, FPrenom et FTel - seront connus sous plusieurs noms. Je préfère les appeler variables instances. La documentation Delphi se rapporte à eux comme étant des champs (je me suis aperçu que cela prête à confusion car nous appelons aussi champs les colonnes d'une base de données). D'autres noms tels que membres de données, attribus et propriétés bien que, comme vous le verrez plus tard, le mot propriété soit aussi utilisé de manière différente dans les modèles d'objets de Delphi.

Si vous êtes famillier du type de données Record de Pascal, vous verrez plus loin, qu'une Classe n'est pas différente d'un Record. Les deux sont des exemples de types de données composites (types de données constituées de plusieurs éléments d'information). Ce qui rend différent un Record d'une Class c'est que vous pouvez écrire du code dans une classe. La classe peut définir des opérations appelées méthodes, que le programme peut utiliser sur les objets. ُDes exemple de méthodes qui pourraient s'appliquer à la classe Etudiant  pourraient être : "AjouterDansUneClasse", "EnvoyerFacture". Regardez dans le fichier d'aide Delphi vous y trouverez quelles méthodes sont disponibles pour les classes TSringList et TIniFile, par exemple.

Ecrire ses propres méthodes

L'écriture des méthodes d'une classe se fait en 2 étapes. La première est de la déclarer dans la partie  déclaration de la classe de la même manière qu'on déclare une instance de variable. Cela permet d'indiquer au compilateur quelles sont opérations pouvant être effectués sur cette classe. Les méthodes sont essentiellement des sous-routines (fonctions ou procédures) et peuvent recevoir des paramètres. Vous devez les déclarer dans la partie déclarations de la classe en indiquant si ce sont des procédures ou des fonctions ainsi que la liste des paramètres qu'elles utilisent. En exemple, voici une classe simple qui déclare 4 instances de variable et 3 méthodes :

Type
  TCarre = Class
  FX, FY : Integer;
  FCote : Integer;
  FEtiquette : String;
  Function Surface : Integer;
  Procedure DeplaceGauche(dx, dy : Integer);
  Procedure DeplaceDroite(dx, dy : Integer);
End;

L'étape suivante consiste à écrire le code correspondant à ces méthodes. Nous verrons ça plus loin, d'abord regardons comment l'utilisateur de ces méthodes pourra les appeler :

Var
  Carre1 : TCarre;
  Aire : Integer;

Begin
    Carre1 := TCarre.Create;
  Try
     Carre1.Fx := 10;
     Carre1.Fy := 20;
     Carre1.FCote := 7;

     Aire := Carre1.Surface ; // On attend 49
     Carre1.DeplaceGauche(2, 3);
  Finally
    Carre1.Free;
End;

Comme vous le voyez on appelle les méthodes en préfixant le nom de méthode du nom de l'objet de la même manière qu'on accède aux variables de l'instance d'un objet. Si vous avez 2 objets en mémoire, et que vous voulez appeler une méthode, quelle instance de l'objet est-ce que la méthode va utiliser ? Posons :

Var
  Carre1 : TCarre;
  Carre2 : TCarre;
  Aire : Integer;

Begin
  Carre1 := TCarre.Create;
  Carre1.FCote := 7;

  Carre2 := TCarre.Create;
  Carre2.FCote := 5;

  Aire := Carre1.Surface; // Est-ce 49 ou 25 ?

Quelle est la valeur de Aire ? Vous vous attendez à ce qu'elle soit de 49 (la valeur du coté du carré X lui-même) ... et c'est ça, mais comme vous pouvez le voir, ce mécanisme peut paraitre mystérieux pour que ça marche. La méthode doit travailler sur les valeurs de l'objet qui l'a appelé. Quand vous écrivez :

Carre1.Surface

la méthode 'surface' doit utiliser les valeurs de l'objet 'Carre1'. vous verrez dans un moment comment ça marche.

Jusqu'ici vous avez vu comment déclarer les méthodes, amis vous devez maintenant écrire le code pour mettre en oeuvre cette méthode. Il faut placer ce code dans la section implementation de l'unité et l'écrire comme la plupart des autres sous-routines. Quand vous déclarez une méthode vous devez spécifier que vous écrivez une méthode de classe et non une sous-routine indépendante. Pour se faire on préfixe le nom de la méthode avec le nom de la classe, tel que :

Function TCarre.Surface : Integer;

et :

Procedure TCarre.DeplaceGauche(dx, dy : Integer);

Notez comment l'objet qui appelle la méthode n'est pas explicitement passé en, paramètre. Donc, comment la méthode (qui est une sous-routine) peut accéder à l'instance de la variable de l'objet qui l'a appelé ? En fait, l'objet est reçu comme un paramètre mais vous ne le voyez pas, et n'avez donc pas à le déclarer. Quand vous écrivez :

Aire := Carre1.Surface; // 49

pensez qu'en fait le compilateur génère le codez suivant :

Aire := Surface( Carre1 );

En arrière plan l'objet est passé en paramètre à la méthode. Dans la méthode il est possible de référencer l'objet qui l'a appelé en utilisant un identifiant prédfini appelé Self. Donc, vous utilisez Self pour accéder aux variables de l'instance de l'objet. Quand vous appelez la méthode avec :

Carre1.Surface

à l'intérieur de la méthode Self fait référence à l'objet Carre1. Quand vous appelez la méthode avec :

Carre2.Surface

Self fait référence à l'objet Carre2.

Voici la méthode Surface complète :

Function TCarre.Surface : Integer;

Begin
  Result := Self.FCote * Self.FCote;
End;

Et voici les méthodes pour DeplacementGauche et  DeplacementDroite :

Procedure TCarre.DeplacementGauche( dx, dy : Integer);

Begin
  Self.Fx := Self.Fx – dx;
  Self.Fy := Self.Fy – dy;
End;

Procedure TCarre.DeplacementDroite( dx, dy : Integer);

Begin
  Self.Fx := Self.Fx + dx;
  Self.Fy := Self.Fy + dy;
End;

La plupart du temps Self est optionnel - vous pouvez simplement faire référence aux variables de l'instance directement :

Function TCarre.Surface : Integer;

Begin
  Result := FCote * FCote;
End;

Ceci fonctionne parce que le compilateur sait à quelle classe appartient la méthode. Il y a une exception à cela, c'est quand vous avez une variable locale pourtant le même nom qu'uen variable de l'instance. Considérons l'exemple suivant :

Function TCarre.Surface : Integer;

Var
  FCote: Integer;
Begin
  Result := FCote * FCote;
End;

Ici, les 2 (variable locale et variable de l'instance) portent le même nom FCote. Dans ce cas le compilateur utilisera la variable locale et la méthode ne fonctionnera pas. Pour corroger ceci vous devez explicitement préfixer la variable de l'instance avec le mot clé Self.

Convention d'appellations

J'ai utilisé quelques conventions d'appellation que je dois expliquer. La convention dans Delphi est de nommer les classes en commençant par la lettre T (mis pour Type). C'est pourquoi npous avons utilisé TCarre et TEtudiant en tant que nom de classe au lieu de simplement Carre et Etudiant. Vous noterez que toutes les classes de la VCL commencent par T tel que : TButton, TForm, etc.

Une autre convention utilisée est de préfixer les variables d'instances avec la lettre F (mis pour Field). Vous verrez la raison d'être de ces conventions plus tard. Pour l'instant suivez-les aveuglément !

Classe MRU List (More Recently Used)

Il y a quantité d'autres notions à acquérir sur la programmation arientée objet, mais au point que nous avons atteint, un véritable exemple vous permettra de fixer dans votre esprit les donnée déjà acquises. Nous allons développer une classe (un nouveau type de données) appelé MRU List (pour gérer une liste des plus récentes chaines de caractères utilisées). On voit des MRU List dans beaucoup d'applications :

  • Les produits Microsoft Office, par exemple, gardent des traces des fichiers utilisés les plus récents.
  • Windows Explorer , les documents récemment utilisés.
  • Delphi la liste des projets récemment ouverts.

Nous développerons une classe que les utilisateurs pourrons utiliser pour conserver une liste des chaines de caractères utilisées récemment (l'utilisateur de la classe pourra conserver la liste les des plus récents clients, fichiers, enregistrements effacés, etc. Parce que nous n'avons pas encore traité de toutes les possibilités du Delphi orienté objet notre classe sera très simple au début (nous n'utiliserons que ce que nous avons appris jusqu'ici).

Commençons par la déclaration des opérations que la classe devra supporter - on ne s'occupe pas de la mise en oeuvre pour l'instant. Voici ce qu'il nous faut :

  • Savoir combien d'éléments contient la liste
  • Possibilité d'ajouter une chaine de caractères à la liste
  • Possibilité de demander ce que contient une cetaine position

Quand on ajoute un élément à la liste, il apparaitra au début de la liste. On décidera du nombre maximum d'éléments à stocker dans la liste (et quand l'utilisateur ajoutera plus que ça l'élement de la fin sera perdu). C'est ce que les anglais appellent une Most Recently Used list.

Voici la première déclaration de cette classe

Type
  TMruListe = Class
  Function Compte : Integer;
  Procedure Ajoute( s : String);
  Procedure Lit( n : Integer) : String
End;

Grace à cette déclaration de classe, voici ce que les utilisateurs pourront faire :

Var
muListe : TMruListe;
s : String;

Begin
  // Instantiation de la classe
  mruListe := TMruListe.Create;
  Try
     // Ajout d'éléments à la liste
    mruListe.Ajoute(‘Spence’);
    mruListe.Ajoute(‘Jones’);

    // Accès à l'élément le plus récent
    s := mruListe.Lit( 0 );
    // s contient 'Jones'
  Finally
    // Penser à libérer la mémoire de l'objet
    mruListe.Free;
  End;
End;

Dans cet exemple on instancie et libère l'objet dans la même routine. Si nous avions utilisé cette classe dans une fiche, on aurait dû instancier la classe dans l'évènement OnCreate et la libérer dans l'évènement OnDestroy.

Nous devons maintenant décider comment mettre en oeuvre cette classe. Nous devons décider comment stocker les chaines de caractères, comment les compter et comment écrire les méthodes. La meilleure solution est probablement d'utiliser la classe TStringist de Delphi pour stocker les éléments. Quand l'utilisateur ajoute un élément à la MRUListe, nous n'avons qu'à l'ajouter au début de la StringList.

Cette solution devra être mise en oeuvre mais nous n'avons pas étudié toute la théorie de la POO pour le faire. Voici le problème. Nous pouvons facilement ajouter une TStringList aux déclarations de notre classe :

Type
  TMruListe = Class
  FMListe : TStringList;
  Function Compte : Integer;
  Procedure Ajoute( s : String);
  Procedure Lit( n : Integer) : String
End;

mais notre classe doit instancier la StringListbut - il nous faut ceci :

FMListe := TStringList.Create;

Instancier la classe MRUListe n'instancie pas la classe TStringList.

Nous pourrions demander à l'utilisateur de la classe d'instancier la stringList après avoir instancié la classe :

mruListe := TMruListe.Create;
mruListe.FMListe := TStringList.Create;

ce n'est pas une riche idée. Vous demandez à l'utilisateur d'effectuer des tâches dans un ordre donné. Sans compter qu'après il doit aussi libérer la StringList avant de libérer la classe MRUListe (c'est trop de responsabilités à confier à l'utilisateur).

Il y a une solution pour cela : écrire la classe MRUListe afin qu'elle instancie et libère la StringList elle-même mais ceci nécessite l'utilisation de constructeur et de destructeur qui seront abordés plus loin. Nous changerons plus tard la classe de cette manière. 

Pour l'instant nous utiliserons donc un tableau de taille fixe pour stocker les chaines et nous aurons une variable séparée pour savoir à tout moment combien d"éléments sont utilisés.

Listing 1 montrant la nouvelle déclaration de classe et la mise en oeuvre de ses méthodes

Const
MRUMaxElements = 4;

Type
  TMruListe = Class
  FMListe : Array[0..MRUMaxElements – 1] of String;
  FNumElements: Integer;
  Function Compte : Integer;
  Procedure Ajoute( s : String);
  Function Lit( n : Integer) : String
End;

Implementation

// Renvoie le nombre d'élements dans la MRUListe
Function TMruListe.Compte : Integer;
Begin
  Result := Self.FNumElements;
End;

// Décale tous les élements dans la liste un par un et en ajoute un au début
Procedure TMruListe.Ajoute(s : String);
Var
  i : Integer;
Begin
  For i := max(Self.FNumElements, MRUMaxElements - 1] DownTo 1 Do
  Self.FMListe[i] := Self.FMListe[i – 1];
  Self.MListe[0] := s;
  Self.FNumElements := Max(FNumElements + 1, MRUMaxElements);
End;

Function Lit( n : Integer ) : String;
Begin
  If (n >= 0) and (n <= Self.FNumElements - 1) Then
  Result := Self.FMListe[n];
End;

Listing 1 – Première version de la classe MRUListe

Portée des instances de variables et des méthodes

Quand on parle de portée nous pensons : où sont-elles visibles (qui peut les utiliser). Dans les classes qu'on a vu jusqu'à maintenant toutes les méthodes et les instances de variables sont visibles des utilisateurs des classes. Ce n'est pas une bonne idée. Considérons l'instance de la variable FNumElements de la classe MRUListe. Cette variable utilisée en interne par la classe pour connaitre le nombre d'éléments de la liste. Parceque cette instance de variable est visible de l'utilisateur de la classe, rien de l'empêche de la changer directement et donc de détruire l'intégrité de notre classe.

Vous devez donc faire la distinction entre les instances de variables et les méthodes visibles des utilisateurs de la classe et celles qui ne doivent être utilisées qu'en interne par la classe. Vous ne devez autoriser l'utilisateur de la classe à ne voir que les variables et méthodes qui constitue l'interface de la classe (que ce qui est indispensable à son utilisation). La mise en oeuvre des détails de la classe (son travail interne) doit être caché de l'utilisateur de la classe. Ceci permet à la personne qui écrit la classe de changer sa mise en oeuvre sans affecter le travail de l'utilisateur de la classe. Tant que l'interface de la classe ne change pas, l'utilisateur n'aura pas à modifier son code.

C'est le second principe de la POO, l'encapsulation.

Introduction

Principe d'Encapsulation....

Pour cacher certaines choses à l'utilisateur, le développeur de la classe sépare ses déclarations en plusieurs sections. Une section détermine la portée de la déclaration s'y trouvant. Une classe peut comporter jusqu'à 4 sections appelées :

Public
Published
Private
Protected

Public

La section Public de la classe contient tout ce que son utilisateur peut voir. Si on ne nomme pas explicitement une section, sa valeur par défaut est public, donc, toutes les méthodes et instances de variables que vous avez vu jusqu'à maintenant ont été publiques.

Private

La section Private de la classe contient tout ce que l'utilisateur ne peux pas voir. Le seul code que les instances et méthodes de cette section peuvent voir sont les autres méthodes de cette classe. Chaque objet contient des instances de variables, mais l'utiisateur de la classe ne peut pas y accéder directement.
Listing 2 montre une seconde version de votre classe TMRUListe qui place NumElements et MListe dans lasection Private. Nous ne montrerons pas le listing du code des méthodes vu que celles-ci ne changent pas.

Type
  TMruListe = Class
Private
  FMListe : Array[0..MRUMaxElements – 1] of String;
  FNumElements : Integer;
Public
  Function Compte : Integer;
  Procedure Ajoute( s : String);
  Function Lit( n : Integer) : String
End;

Listing 2 – Seconde version de la classe MRUListe utilisant des instances de variables privées.

Maintenant l'utilisateur de la classe ne peut plus utiliser ni FMListre ni FNumElements. Nous avons restreint l'utilisation de ces instances de variables aux méthodes de cette classe. Ce sont ces données privées que l'utilisateur n'a pas besoin de voir. C'est l'encapsulation cachant la mise en oeuvre des détails de la classe.

Protected

La section Protected est en rapport avec la notion d'héritage, je réserve à plus tard la description détaillée jusqu'à ce qu'on traite de ce sujet. Brièvement, les instances de variables et les méthodes que vous placez dans la section Protected sont invisibles aux utilisateurs de la classe, de ce fait elles ressemblent aux Private. La différence entre Privated et Protected se situe au niveau des sous-classes. Les méthodes des sous-classes ne peuvent rien voir de ce qu'une classe parente a déclaré Private mais peuvent voir ce que les superclasses ont déclaré Protected.

Published

La section Published de la classe est très similaire à la section Public ; toutes les 2 listent des instances de variables que l'utilisateur peut voir. La différence entre ces 2 sections concerne les composants. Les composants sont simplement des classes que vous pouvez utiliser dans l'IDE Delphi que l'utilisateur peut déposer dans une fiche et manipuler visuellement. Les boutons radio, les boutons, etc. sont des composants. Toutes les classes ne sont pas des composants, bien sûr, mais tous les composants sont des classes.

Quand votre classe est un composant, tout ce que vous mettez dans la section Published est disponible dans l'inspecteur d'objets. Ceci permet à l'utilisateur d'affecter des valeurs à ces instances de variables en utilisant l'inspecteur d'objets au lieu de faire ces même affectations par code.

Ecrire ses propres constructeurs & destructeurs

Vous savez déjà ce qu'est un constructeur (une fonction spéciale qu'on appelle pour instancier une classe). Vos classes peuvent déclarer leurs propres constructeurs de la même manière que des méthodes normales. L'avantage d'écrire vos propre construteurs réside dans le fait que vous pouvez faire des initialisations quand la classe est instanciée.

Par exemple, considérons le code suivant qui instancie une classe TCarre puis initialise quelques unes de ses instances de variables :

o := TCarre.Create;
o.FX := 10;
o.FY := 10;
o.FCote := 5;
o.FEtiquette := ‘Premier carré’;

Si vous aviez écrit votre propre constructeur pour la classe TCarre elle aurait pu recevoir les valeurs initiales et permettre à l'utilisateur d'écrire :

o := TCarre.Create( 10, 10, 5, ‘Premier carré’);

C'est certainement plus pratique mais ça a aussi d'autres avantages. En fournissant un constructeur, le développeur de la classe peut s'assurer que son objet est correctement initialisé. Par exemple considérons l'utilisation suivante de la classe TCarre qui ne possède pas de constructeur.

o := TCarre.Create;
// Calcul de sa surface
a := o.Surface;

Le code appelle la méthode Surface mais l'utilisateur a oublié de, d'abord, initialiser le côté du carré. Si la classe avait fourni un construteur l'utilisateur aurait eu à passer le paramètre Cote (s'il avait oublié, le compilateur lui aurait rappelé).

Un autre avantage des constructeurs est qu'ils peuvent instancier des objets associés pour vous. Plus avant dans cet article nous avons précisé qu'il serait plus pratique d'utiliser des TStringList dans la TMRUListe pour stocker les éléments. Nous avons dit que ce serait maladroit parce que nous aurion dû instancier la classe TStringList. Hé bien le constructeur est la place idéale pour placer ça. Quand l'utilisateur instancie la classe TMRUListe, son construteur va instancier la classe TStringList.

Maintenant la TStringlist a besoin d'être libérée. Quand voulez-vous que ce soit fait ? Quand la TMRUListe elle-même est libérée. Ce n'est cependant pas automatique. Quand la TRUListe est libérée vous devez exécuter un bout de code qui va libérer la stringlist. Le modèle objet de Delphi fourni pour cela une mot clé appelé destructor. Le destructeur est le contraire du constructeur – il est appelé quand l'objet est détruit. Nous verrons les destructeur plustard, voyons déjà la syntaxe des constructeurs.

On déclare un constructeur dans la déclaration de classe en utilisant le mot clé constructor :

Type
  TCarre= Class
  FX, FY : Integer;
  FEtiquette : String;
  FCote: Integer;
  Function Surface : Integer;
  Constructor Create( px, py : Integer;
  pCote: Integer; pEtiquette : String;
End;

puis on écrit son code dans la section implementation en utilisant encore le mot clé Constructor:

Constructor TCarre.Create(px, py : Integer;
pCote: Integer; pEtiquette : String);
Begin
  Self.FX := px;
  Self.FY := py;
  Self.FCote := pCote;
  Self.FEtiquette := pEtiquette;
End;

Comme on peut le voir tout ce que fait le constructor est de copier les paramètres qu'il reçoit dans les instances de variables. Dans ce cas Self est optionnel. As you can see, all the constructor is doing is copying the parameters it receives into the instance variables. In this case, Self is optional. Cepandant si un paramètre portait le même nom qu'une instance de variable, on devrait utiliser Self à gauche de l'instruction d'affectation pour forcer le compilateur d'utiliser une instance de variable au lieu du paramètre.

Le code suivant montre le constructeur de la classe MRUListe tenant compte du fait que la classe va utiliser une StringList appelée FMListe pour stocker les chaines de caractères les plus récemment utilisées.

Constructor TMRUListe.Create;

Begin
  FMListe := TStringList.Create;
End;

Le constructeur instanticie simplement la classe TStringList et la sauvegarde dans une instance de variable appelée FMListe. Comme nous l'avons déja précisé, la classe doit maintenant libérer la stringlist quand la classe MRUListe est elle-même détruite. Vous devez ajouter le code correspondant dans le destructeur de la classe.

Delphi appelle automatiquement le destructeur de la classe  quand vous détruisezez l'objet :

mruListe := TMRUListe.Create;
Try
   // traitement avec mruListe ici
Finally
   mruList.Free; // Ceci appelle le destructeur
End;

Quand votre code appelle Free, Delphi appelle automatiquement le destructeur de votre classe. Vous déclarez votre destructeur dans la déclaration de classe en utilisant le mot-clé Destructor. Pour des raisons que nous verrons juste après les destructeurs sont toujours appelés Destroy. Vous devez aussi déclarer un destructeur Override – cela aussi vous verrez plus loin pourquoi - si le destructeur n'est pas déclaré override il ne sera jamais exécuté. Voila la nouvelle déclaration de classe illustrant le constructeur, destructeur et la stringList.

Type
  TMruListe = Class
Private
  FMListe : TStringList;
  Constructor Create;
  Destructor Destroy; Override;
Public
  Function Compte : Integer;
  Procedure Ajoute( s : String);
  Function Lit( n : Integer) : String
End;

Pour implement le destructeur on met son code dans la section implémentation comme on le ferait pour le constructeur. On utilise le mot-clé Destroy pour introduire la méthode. Le destructeur doit appeler une  méthode de superclasse de même nom après avoir effectué le travail. Ceci grace à l'utilisation du mot-clé inherited :

Destructor TMRUListe.Destroy;
Begin
  FMListe.Free;
  Inherited Destroy;
End;

Note : l'instance de variable 'NumElements' a été enlevée de la classe puisque cette valeur peut être déduite de stringList elle-même. Listing 3 montrant le code remanié :

// Renvoie le nombre d'éléments de la MRUListe
Function TMruListe.Compte : Integer;
Begin
  Result := Self.FMListe.Count;
End;

// Décale tous les éléments de la liste de 1,
// ajoute un nouvel élément au début
Procedure TMruListe.Ajoute(s : String);
Var
  i : Integer;
Begin
  Self.FMListe.Insert(0, s);
  If Self.FMListe.Count >= MRUMaxElements Then
  Self.FMListe.Delete( Self.FMListe.Compte – 1);
End;

Function Lit( n : Integer ) : String;
Begin
  If (n >= 0) and (n <= Self.FMList.Count - 1) Then
    Result := Self.FMListe.Strings[n];
End;

Listing 3 : troisième version de la classe MRUListe utilisant une StringList

Noter que même si la manière dont la classe fonctionne a complètement changé le code qui utilise la classe ne changera pas du tout parce que l'interface est toujours la même. C'est le role de l'encapsulation.

Résumé

C'était une rapide introduction à la POO avec Delphi. ll y a encore beaucoup de choses à dire pour approfondir ce que cet article a abordé.

 

Auteur

Rick Spence est le directeur technique de Web Tech Training and Development, une companie avec des bureaux aux USA et UK.