[delphi] développer des classes

[delphi] développer des classes

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...

Déclarer une classe

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;

Construire et détruire l'objet

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.

Déclarer une procédure et une fonction

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...

Les propriétés

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

  • On déclare une variable dans PRIVATE avec spécification du type,
  • On publie la propriété Message dans PUBLISHED,
  • On initialise dans le constructeur.

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 :

  • Objet.Message:='Salut' fait appel au WRITE car on assigne 'Salut' dans la variable FMessage
  • ShowMessage(Objet.Message) fait appel au READ car on récupère le contenu du message pour l'afficher

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 :

  • De déclarer la procédure SetMessage dans la clause PRIVATE
  • De nommer la procédure SetQuelqueChose avec un "SET" au début (bonnes habitudes obligent)
  • De ne mettre qu'un seul paramètre nommé Value (idem) du même type que la propriété

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 !

Utiliser sa classe

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 :

  • elle n'est créée qu'une seule fois, ce qui limite les bribes en mémoire si le Free n'est pas complet à 100% (avec TBitmap ça défonce et rien que pour ça, je lui met 0/20)
  • on a un objet à tout faire (le risque étant que vous vous emméliez les pinceaux, mais qui est limité complètement dès que vous avez une vision très bonne du fonctionnement de la classe).

Les dangers des publications

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.

Conclusion

En mixant un peu le tout, on arrive à faire des choses sympathiques.

Tanguy ALTERT, http://altert.family.free.fr/

A voir également
Ce document intitulé « [delphi] développer des classes » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous