Le tutoriel 177 (Déclarer et utiliser les types en Delphi) nous laissait en reste sur les classes pour la simple et bonne raison que ça méritait plutôt un beau chapitre complètement dédié. Après avoir lu tout ce qui suit, vous serez prêts pour attaquer le développement de composants (car ce sont des classes). Mais avant, relisez bien le tutoriel 177 si vous avez quelques problèmes.
J'aurai tendance à équivaloir les mots OBJET et CLASSE. Les classes doivent être créées puis détruites. Pour un objet, c'est pareil : il ne tombe pas du ciel comme çà. De plus, un objet a une forme, des propriétés, un fonctionnement... Bref, on fait des objets ! D'où le nom de programmation orientée objet (POO) qui ne gère presque que des classes.
Allez, on y va...
Ce n'est pas compliqué. Par défaut, on écrit d'abord ceci :
type TMaClasse = class private public protected published end;
On n'oublie pas le "T" de TMaClasse... et on observe 4 rubriques différentes.
Qui sont-elles ?
Pour avoir de l'aide, cliquez une fois sur les mots en gras et faites F1 (dans Delphi bien sûr ;).
PRIVATE : permet de déclarer des variables, des procédures et des fonctions internes à la classe. De ce fait, aucune classe extérieure (parente ou enfante) ne peut agir sur cette section. Une fois la classe compilée, il n'y a plus aucun recours pour pouvoir modifier cette section (une dérivation de classe ne libère pas le contenu).
PUBLIC : met les variables, les procédures et les fonctions qui y sont déclarées à disposition du développeur et des classes dérivées (on notera plus tard les dangers de cette partie).
PROTECTED : Dans la VCL de Delphi, cela permet de déclarer secrètement des propriétés qui ne peuvent être visibles qui si on dérive la classe et si on publie ce qui est protégé. Les trois autres sections sont plus fondamentales.
PUBLISHED : pour les variables, procédures et fonctions, ça a le même comportement que PUBLIC, mais PUBLISHED sert à indiquer à Delphi quelles propriétés seront visible dans l'inspecteur d'objet dans le cas où vous développez des composants.
Voilà une classe opérationnelle la plus simple qui puisse exister. Vous me direz que cette classe n'est en fait pas vide, car Delphi montre ostensiblement qu'un corps de fonctions et de procédures est déjà greffé. Ça, c'est un problème technique géré par Delphi... Sachant qu'on ne peut pas accéder à certains éléments (nom de la classe en format STRING par exemple) par la programmation Pascal, Delphi offre par défaut un package de base auto-généré qui fait passer outre les problèmes. Si Delphi ne faisait pas ça, on serait bien dans la m*****... ;). De plus, ça intègre des fonctionnalités avancées pour la hiérarchisation des classes.
Savoir si une classe est dérivée revient à dire s'il y a un paramètre après CLASS :
type TClassePrimitive = class end; TClasseDerivee = class(TComponent) end;
Toute classe nécessite d'être créée puis détruite (ordre logique). Les autres types du tutoriel 177 étaient prêts à l'emploi, mais là, ça ne rigole plus. Il faut un CONSTRUCTOR et un DESTRUCTOR.
Pour l'instant, notre classe n'est pas dérivée. Avec le développement de composants, ce sera bien différent et plus difficile. De ce fait, le constructeur et le destructeur ont une forme totalement libre (je veux dire par là qu'on peut leur ajouter des paramètres). Il faut les considérer comme des procédures très spéciales (repérées par le linker).
unit ClassRoom; interface uses SysUtils; type TMaClasse = class private FVariable : string; public constructor Create(Param:string); destructor Destroy; end; implementation constructor TMaClasse.Create(Param:string); begin FVariable:=Param; end; destructor TMaClasse.Destroy; begin end; end.
Le CONSTRUCTOR alloue implicitement de la mémoire et permet d'initialiser les variables internes (déclarées nécessairement dans PRIVATE). J'ajouterai qu'en obligeant la classe à être créée, ça force son initialisation.
Le DESTRUCTOR est appelé lorsque la classe va être détruite et c'est à ce moment qu'il faut faire toutes les libérations de mémoire s'il y en a eu (dans l'ordre inverse des créations). Par exemple, si vous avez utilisé un TPanel (c'est un objet) dans cette classe, il a été créé dans le CONSTRUCTOR, il faudra donc l'éliminer dans le DESTRUCTOR avant que la classe elle-même ne subisse le même traitement. Si on crée Panel1 et Panel2, on supprimera d'abord Panel2 puis Panel1 pour éviter les chevauchements qui sont interdits en Pascal (voir le code JPG de ni69 pour lire un de mes commentaires sur ce sujet).
Il s'avère que seules les classes doivent être libérées. Tous les autres types (string, array, record...) sautent tous seuls... Étant donné que notre exemple ne gère qu'un STRING, il est logique qu'il n'y ait rien dans le DESTRUCTOR. On peut donc effacer le DESTRUCTOR et sa déclaration, car (ici) il est inutile donc superflu. Cependant, il existe toujours. Pour être clair, il existait déjà avant même que vous ne le tapiez vous même. Vous vous souvenez de ma remarque: «Delphi offre par défaut un package de base auto-généré». Là, la classe n'est pas dérivée. Donc, le fait que vous ayez déclaré vous même un DESTRUCTOR ne crée pas de problèmes. L'original sera substitué au nouveau, sachant que de toute manière Delphi vous cachera du code une fois le END du destructeur atteint.
C'est à ce moment que les sérieux du Delphi vont dire: «Mais où est INHERITED ?». Nul part bien sûr... on ne fait pas encore de composants (il n'y a pas eu de dérivation) !! Ca va venir... et OVERRIDE va débouler 8=)
Bref... méditez le morceau de code précédent, puis le suivant :
unit ClassRoom; interface uses SysUtils, ExtCtrls; type TMaClasse = class private FPN : TPanel; public constructor Create(Param:string); destructor Destroy; end; implementation constructor TMaClasse.Create(Param:string); begin FPN:=TPanel.Create; end; destructor TMaClasse.Destroy; begin FPN.Free; end; end.
C'est toujours un jeu d'enfants. Regardez :
unit ClassRoom; interface uses SysUtils, Dialogs; type TMaClasse = class private procedure UtilisationInterne(Message:string); public function Aleatoire:integer; end; implementation procedure TMaClasse.UtilisationInterne; begin ShowMessage(Message); end; function TMaClasse.Aleatoire:integer; begin Aleatoire:=Random(50); end; end.
N'ayant pas de variable à initialiser et étant au sein d'une classe non dérivée (n'oublions aucune hypothèse), je n'ai pas besoin de faire un CONSTRUCTOR, et pas besoin non plus d'un DESTRUCTOR. Par défaut, ils sont cachés.
J'utilise ici la fonction RANDOM qui requiert un appel unique à la fonction RANDOMIZE. Je préfère laisser le programmeur utiliser RANDOMIZE qu'il implémentera dans le fichier DPR de son projet. Si j'avais mis RANDOMIZE dans le constructeur, si je dois créer deux variables de type TMaClasse, alors je fais deux CREATE, donc j'appelle deux fois RANDOMIZE ==> je faillis à la règle.
Avez-vous remarqué que sous IMPLEMENTATION, UtilisationInterne n'expose pas son paramètre Message ?
Par héritage, ayant écrit procedure TMaClasse.UtilisationInterne, Delphi va rechercher dans PRIVATE les paramètres manquants, ce qui ne crée pas de problèmes. Cependant, par souci de lisibilité, il est vivement conseillé de mettre le paramétrage en double (quitte à devoir faire plus de modifications), afin de ne pas avoir à jouer de l'ascenseur pour rechercher visuellement ce qu'on a omis d'écrire (ahh, la flemme !... ;)
Vous commencez à sentir le truc...
Ça aussi, c'est ultra méga important pour les composants... Regardez le code suivant (on a un constructeur, car on doit initialiser des variables) :
unit ClassRoom; interface uses SysUtils; type TMaClasse = class private FMessage : string; public constructor Create; published property Message:string read FMessage write FMessage; property Message_ReadOnly:string read FMessage; end; implementation constructor TMaClasse.Create; begin FMessage:=''; end; end.
Le principe est très très simple :
J'ai rajouté Message_ReadOnly pour montrer comment on fait pour avoir une propriété en lecture seule. Par principe inverse, on peut avoir écriture seule.
Comment interpréter le READ et le WRITE de PROPERTY ?
C'est un peu comme pour les RECORD :
Peut-être voudriez vous faire un traitement lors du WRITE ?
Sur ce point, les CLASS ont un avantage certain sur les RECORD.
Eh bien, vous faîtes comme cela :
unit ClassRoom; interface uses SysUtils; type TMaClasse = class private FMessage : string; procedure SetMessage(Value:string); public constructor Create; published property Message:string read FMessage write SetMessage; end; implementation constructor TMaClasse.Create; begin FMessage:=''; end; procedure TMaClasse.SetMessage(Value:string); begin if Value<>FMessage then begin FMessage:=Value; ShowMessage(FMessage); end; end; end.
Dans cette classe, lorsqu'on modifie (par le WRITE) la propriété Message, le contenu du nouveau message est affiché au programmeur. C'est franchement inutile, mais c'est juste pour illustrer l'effet.
Il est important :
On doit même pouvoir faire pareil dans l'autre sens avec le READ. Il suffit alors de transformer procedure SetMessage(Value:string) en function GetMessage:string avec cette fois un "GET" au début. On aurait alors :
procedure TMaClasse.GetMessage:string; begin GetMessage:=FMessage; ShowMessage(FMessage); end;
!!!!!! ATTENTION !!!!!!
Au sein d'une même classe, utilisez ABSOLUMENT les variables F* pour récupérer le contenu des propriétés. L'exemple précédent est correct. Le suivant est interdit :
procedure TMaClasse.GetMessage:string; begin GetMessage:=FMessage; ShowMessage(Message); end;
Cet appel à Message enchaîne un READ, donc un GetMessage et on aurait un équivalent en référence circulaire (une boucle sans fin quoi) :
procedure TMaClasse.GetMessage:string; begin GetMessage:=FMessage; //ici on assigne le résultat de sortie (no problemo) ShowMessage(GetMessage); end;
Utilisez TOUJOURS les variables privées et pas les publications !
On a créé un fichier CLASSROOM.PAS qui contient ceci :
unit ClassRoom; interface uses SysUtils; type TMaClasse = class private FMessage : string; public constructor Create; published property Message:string read FMessage write FMessage; end; implementation constructor TMaClasse.Create; begin FMessage:=''; end; end.
Vous avez un nouveau projet, donc un fichier UNIT1.PAS.
Si vous gérez une variable globale (non cantonnée à une procédure), vous avez ceci. La variable Clss est prête à l'emploi et sera détruite automatiquement lorsque l'application se fermera.
unit Unit1; interface uses Windows, SysUtils, Classes, Controls, Forms, Dialogs, ClassRoom; // Ajouter ClassRoom type TForm1 = class(TForm) private { Déclarations privées } public { Déclarations publiques } end; var Form1 : TForm1; Clss : TMaClasse; // A AJouter implementation {$R *.DFM} initialization // A AJouter Clss:=TMaClasse.Create; // A AJouter finalization // A AJouter Clss.Free; // A AJouter end.
Si vous gérez un évènement OnClick sur un bouton, alors vous obtenez plutôt ceci :
unit Unit1; interface uses Windows, SysUtils, Classes, Controls, Forms, Dialogs, ClassRoom; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private { Déclarations privées } public { Déclarations publiques } end; var Form1 : TForm1; implementation {$R *.DFM} procedure TForm1.Button1Click(Sender: TObject); var Clss : TMaClasse; begin try [...] finally Clss.Free; end; end; end.
Dans ces cas extrêmes, il faut toujours faire un TRY...FINALLY...END afin d'être sûr que l'objet sera libéré. Si on avait pas le TRY, si lors du [...] il se produit une exception, alors le FREE ne serait pas appelé et l'objet resterait en mémoire. C'est pas très propre...
Pour ce qui concerne l'affrontement entre ces deux codes, il est préférable de faire une variable globale :
Les classes encapsulent de nombreux traitements mais ce n'est pas pour autant un cocon hermétique intouchable. Il faut donc offrir au développeur le strict nécessaire afin qu'il puisse puiser le maximum de ressource dans la classe sans altérer son fonctionnement.
PUBLIC est dans le collimateur... Quoi donner et quoi ne pas donner ? Il faut déjà un peu de jugeote !
Imaginons une classe qui comporte en elle-même une procédure de "formatage de disque" [c'est complètement idiot, mais admettons]. Il est clair que cette procédure ne doit pas être publiée. Préférez plutôt publier une fonction dont la tâche serait de déclencher ce hara-kiri selon certaines modalités. C'est comme les jeunes bambins, il faut les encadrer : on laisse libre, mais on ne veut pas le bazar.
Il faut réfléchir sur ce dont on a besoin, et sur ce qu'on met à disposition du développeur.
En mixant un peu le tout, on arrive à faire des choses sympathiques.
Tanguy ALTERT, http://altert.family.free.fr/
6 oct. 2019 à 22:37