Agrégation, composition avec Delphi

Neil Baumberger

Voici une illustration de la belle mécanique offerte par Delphi en ce qui concerne l'application des notions d'UML que sont : l'agrégation et la composition d'objets.

- Titre : Agrégation, composition avec Delphi

- Auteur : Neil Baumberger

- Parution : 3 septembre 2005

- Mise à jour : 21 mai 2020

- Licence : Copyright ® 2005 Neil Baumberger. 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

- I. Rappels en UML

- II. L'exemple en théorie

- II-A. But à atteindre

- II-B. Modélisation

- II-C. Explications

- II-C-1. Losange rempli, losange vide ?

- II-C-2. Justifications de l'existence de la classe TCustomTele

- II-C-3. Justifications de l'existence de la classe TCustomLecteur

- III. L'exemple en pratique

- III-A. Déclaration des classes

- III-A-1. Les télés

- III-A-2. Les lecteurs de médias

- III-B. Implémentation

- III-B-1. Agrégation

- III-B-2. Composition

- III-C. Empaquetage

- III-D. Phase de test

- III-E. Constatations

- Conclusion

I - Rappels en UML

Inutile de refaire un travail déjà très bien fait. Donc, si vous avez besoin de revoir (ou d'apprendre) ces notions d'UML, rien ne vaut : Agrégation et Composition en UML.

II - L'exemple en théorie

II-A - But à atteindre

Modéliser les objets suivants :

- magnétoscope ;

- télé ;

- lecteur DVD ;

- télé combinée avec un magnétoscope (deux en un) ;

- télé combinée avec un lecteur DVD (deux en un).

Contrainte : il faudrait pouvoir brancher aussi bien un magnétoscope qu'un lecteur DVD sur la télé normale.

II-B - Modélisation

Plusieurs approches sont, comme toujours, possibles : cela dépend du besoin réel.
Ici nous tentons d'illustrer l'agrégation et la composition, ainsi, nous pouvons commencer par regrouper nos objets et les catégoriser.

Lecteur de média :

- magnétoscope ;

- lecteur DVD.

Type de télévision :

- télé de base ;

- télé combo lecteur DVD ;

- télé combo magnétoscope.

Diagramme UML

Pour ne pas surcharger le schéma inutilement, la classe TTeleComboMagnetoscope a été omise volontairement.

II-C - Explications

Un certain nombre de remarques peuvent être faites au sujet de ce diagramme lorsqu'on débute.

Les principales remarques sont :

- quelle différence y a-t-il entre le losange rempli et le losange vide ?

- à quoi servent les classes TCustomXXX ?

II-C-1 - Losange rempli, losange vide ?

Voir ou revoir le cours d'UML car ce sont des symboles propres à UML.

II-C-2 - Justifications de l'existence de la classe TCustomTele

En prévision d'éventuelles propriétés telles que la diagonale de l'écran, le statut Marche/Arrêt, il est préférable de regrouper ces caractéristiques communes en un seul ancêtre.

Ensuite, les descendants n'auront plus qu'à faire leur travail spécifique.

Pour TTele

- Mettre à disposition une agrégation avec un TcustomLecteur.

Pour les TTeleComboXXX

- Étant composés d'un lecteur, ils doivent créer et libérer leur propre lecteur.

II-C-3 - Justifications de l'existence de la classe TCustomLecteur

Une classe ancêtre est nécessaire afin de centraliser un certain nombre de caractéristiques et aussi dans l'optique d'interchanger de lecteur pour une télé normale.

III - L'exemple en pratique

Voyons maintenant la mise en pratique de ce modèle UML.
Nous allons donc voir comment Delphi nous propose des mécanismes prévus exactement pour ces besoins de conception.

Les commentaires dans le code sont importants pour la compréhension.

III-A - Déclaration des classes

III-A-1 - Les télés

{ Classe de base pour tout téléviseur ------------------------------------- }
TCustomTele = class(TComponent)
private
FAllumee: Boolean;
FDiagonaleCM: Integer;
public
constructor Create(AOwner: TComponent); override;
published
property Allumee: Boolean read FAllumee write FAllumee;
property DiagonaleCM: Integer read FDiagonaleCM write FDiagonaleCM;
end;

{ Classe standard représentant une télé courante ----------------------------}
TTele = class(TCustomTele)
private
FLecteur: TCustomLecteur;
procedure SetLecteur(Value: TCustomLecteur);
protected
// Important :
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
function LecteurPresent: Boolean;
published
// Agrégation d'un lecteur externe à la télé (ex. : Magnétoscope, LecteurDVD)
property Lecteur: TCustomLecteur read FLecteur write SetLecteur;
end;

{ Les combinés --------------------------------------------------------------}
TTeleComboDVD = class(TCustomTele)
private
FLecteurDVD: TLecteurDVD;
procedure SetLecteurDVD(const Value: TLecteurDVD);
public
constructor Create(AOwner: TComponent); override;
published
// Composition d'un LecteurDVD
property LecteurDVD: TLecteurDVD read FLecteurDVD write SetLecteurDVD;
end;

TTeleComboMagneto = class(TComponent)
private
FMagnetoscope: TMagnetoscope;
procedure SetMagnetoscope(const Value: TMagnetoscope);
public
constructor Create(AOwner: TComponent); override;
published
// Composition d'un Magnétoscope
property Magnetoscope: TMagnetoscope read FMagnetoscope write SetMagnetoscope;
end;

III-A-2 - Les lecteurs de médias

{ Classe de base pour tout lecteur de médias --------------------------------}
TCustomLecteur = class(TComponent)
public
procedure Eject; virtual;
procedure Play; virtual;
procedure Stop; virtual;
end;

TLecteurDVD = class(TCustomLecteur)
private
FCompatibleMP3: Boolean;
published
// Exemple de particularité propre au lecteur DVD
property CompatibleMP3: Boolean read FCompatibleMP3 write FCompatibleMP3;
end;

TMagnetoscope = class(TCustomLecteur)
private
FNbeTetesLecture: Integer;
published
// Exemple de particularité propre au magnétoscope
property NbeTetesLecture: Integer read FNbeTetesLecture write FNbeTetesLecture;
end;

III-B - Implémentation

Nous allons nous arrêter sur ce qui concerne notre sujet en commençant par l'agrégation de notre TTele puis notre composition avec un TTeleComboLecteurDVD.

III-B-1 - Agrégation

procedure TTele.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
// Si un composant nous notifie qu'il est en cours de destruction...
if Operation=opRemove then
begin
//... et que c'est notre lecteur actuel alors...
if AComponent=FLecteur then
FLecteur := nil; // On ne garde pas une référence qui deviendra fausse.
// Ainsi, pour d'éventuelles utilisations, dans la classe TTele, du Lecteur
// il faut bien faire attention que le Lecteur soit toujours
// assigné ( if Assigned(FLecteur) then .... else ... )
end;
end;

procedure TTele.SetLecteur(Value: TCustomLecteur);
begin
// Si c'est un lecteur autre que l'actuel :
if FLecteur<>Value then
begin
// Si l'actuel existe encore
if Assigned(FLecteur) then
FLecteur.RemoveFreeNotification(Self); // Je ne souhaite plus être notifié par lui de sa destruction

// Le nouveau lecteur devient l'actuel
FLecteur := Value;

// Si le nouveau est bien un lecteur (et pas rien (nil))
if Assigned(FLecteur) then
FLecteur.FreeNotification(Self);  // Je souhaite qu'il me notifie à temps de sa destruction éventuelle
end;
end;

III-B-2 - Composition

constructor TTeleComboDVD.Create(AOwner: TComponent);
begin
inherited;
// Je crée le lecteur qui m'appartient (ainsi il sera libéré en même temps que moi)
FLecteureDVD := TLecteurDVD.Create(Self);

// Je précise à Delphi qu'il doit faire persister l'état de l'objet FLecteureDVD
FLecteureDVD.SetSubComponent(True);

// Pour l'esthétique ou bien dans le cas où j'aurais plusieurs LecteurDVD en SubComponent(True)
// je nomme le composant
FLecteureDVD.Name := 'LecteurDVD';
end;

Pour plus de précisions sur la mécanique du TComponent.Create(AOwner: TComponent) se référer à l'aide Delphi.
Même remarque pour le TComponent.SetSubComponent() mais touchons-en malgré tout quelques mots.

Il faut savoir que le code FLecteureDVD.SetSubComponent(True); change quelque peu l'état de l'objet FLecteureDVD.
Ainsi, lorsque Delphi tentera de sauvegarder la form sur laquelle est posée notre TTeleComboDVD, il saura qu'il faut également enregistrer l'état de FLecteureDVD (qui est bien sûr une propriété en published).
Même chose lors du chargement de la form, l'état de FLecteureDVD sera rétabli.

III-C - Empaquetage

Vous trouverez la façon dont le tout a été rangé dans le fichier sources.zip
Si ce lien ne fonctionne pas chez vous, utilisez celui-ci.zip.

III-D - Phase de test

Le test consiste simplement à déposer une instance de chacun des composants qui apparaissent désormais dans la palette à l'onglet 'TV-Vidéo'.

Télé et lecteurs :

- on peut alors « brancher » un magnétoscope ou bien un lecteur DVD à la télé ;

- si l'on supprime un lecteur qui était branché à la télé, tout se passe bien (pas de violation d'accès).

Télés combo :

- nos combos ont leur propre lecteur bien accessible par l'inspecteur d'objet ;

- mais ni interchangeable ni supprimable.

III-E - Constatations

Pour les curieux, vous pouvez regarder le .dfm qui nous éclaire sur la différence entre agrégation et composition :

object Form1: TForm1
Left = 198
Top = 108
Width = 870
Height = 600
Caption = 'Form1'
object Tele1: TTele
Allumee = False
DiagonaleCM = 45
Lecteur = Magnetoscope1 // Tele1.Lecteur fait référence à l'instance Magnetoscope1
Left = 96
Top = 72
end
object TeleComboDVD1: TTeleComboDVD
Allumee = False
DiagonaleCM = 45
LecteureDVD.CompatibleMP3 = False // LecteureDVD est propre à TeleComboDVD1
Left = 96
Top = 160
end
object Magnetoscope1: TMagnetoscope
NbeTetesLecture = 0
Left = 192
Top = 72
end
end

Conclusion

En guise de conclusion, on ne peut que vous inviter à explorer la VCL qui recèle de nombreux trésors d'utilisation de l'approche objet.
Ainsi vous trouverez l'utilisation de SetSubComponent(True) dans l'unité StdActns :

constructor TCommonDialogAction.Create(AOwner: TComponent);
var
DialogClass: TCommonDialogClass;
begin
inherited Create(AOwner);
DialogClass := GetDialogClass;
if Assigned(DialogClass) then
begin
FDialog := DialogClass.Create(Self);
FDialog.Name := Copy(DialogClass.ClassName, 2, Length(DialogClass.ClassName));
FDialog.SetSubComponent(True);
end;
DisableIfNoHandler := False;
Enabled := True;
end;

et, beaucoup plus fréquemment, l'implémentation de Notification() (exemple : dans l'unité ActnList) :

procedure TCustomActionList.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if Operation = opRemove then
if AComponent = Images then
Images := nil
else if (AComponent is TContainedAction) then
RemoveAction(TContainedAction(AComponent));
end;