Les références circulaires
Le langage Delphi permet de découper le projet en différentes Unités. Les références entre unités sont explicites, la clause Uses liste les unités référencées. Cette approche étant strictement arborescente, on peut être confronté au problème de référence circulaire.NB: ce problème est particulier au Pascal qui possède un compilateur une passe, c'est à dire qu'avant de compiler une unité, le compilateur doit compiler toutes les unités qu'elle utilise...
Le problème se pose sous la forme : l'unité A utilise l'unité B qui utilise l'unité A (Qui de la poule ou de l'oeuf...)
Il est important de noter qu'il est possible pour deux unités de s'entre référencer à condition qu'au moins l'une des deux le fasse dans sa partie Implementation. En effet le compilateur prend soin de compiler l'unité en deux étapes, d'abord l'Interface qui doit être pleinement définie pour pouvoir apparaitre dans une clause Uses, et ensuite l'Implementation qui supporte alors les références croisées.
Voici un exemple de référence croisée
{ unité principale } unit Unit1; interface uses Unit2; // l'Interface de Unit2 est compilée en premier implementation // arrivé ici, le compilateur connait l'Interface de Unit1 // il peut donc compiler la partie Implementation de Unit2 end.
{ unité secondaire } unit Unit2; interface // la référence a Unit1 est ici INTERDITE ! implementation uses Unit1; // ...elle est autorisée ici end.
C'est bien beau tout cela, mais que ce passe-t-il si dans la partie Interface de Unit2 je dois faire référence a un objet déclaré dans Unit1 (et inversement) ?
Pour illustrer mon propos, je vais prendre un exemple tiré du livre Delphi 7 Studio (éditions Eyrolles) que j'ai co-écrit avec Oliver DAHAN.
Le chapitre 21 propose un exemple d'utilisation des collections, il ne présente aucun problème de référence car les trois classes en jeu sont déclarées dans une seule et même unité :D :
{ version épurée de l'unité 2 de l'exemple } type TCDComponent = Class; (1)// pré-déclaration du composant TMP3Item = Class(TCollectionItem) protected function GetDisplayName: string; override; public constructor Create(Collection: TCollection); override; end; TMP3Collection = Class(Tcollection) private fCDcomp : TCDComponent; (2) protected function GetOwner:TPersistent; override; procedure Update(Item: TCollectionItem); override; public constructor create(CD:TCDComponent); function Add : TMP3Item; end; TCDComponent = Class(Tcomponent) private fItems : TMP3Collection; (3) public procedure ListeModifiee(Item:TMP3Item); end;
La première déclaration de
TDCComponent
(1) permet à TMP3Collection
de déclarer un membre
fCDComp
(2) sur une classe déclarée plus loin dans le source; inverser l'ordre des déclarations ne changerait
pas le problème car TCDComponent
fait lui-même référence à TMP3Collection
(3) ;DNous allons maintenant éclater cette unité en trois, une unité par classe :
unit unit2; interface uses Classes,unit3,unit4; type TCDComponent = Class(Tcomponent) private fItems : TMP3Collection; public procedure ListeModifiee(Item:TMP3Item); end;
unit unit3; interface uses Classes,unit2,unit4; type TMP3Collection = Class(Tcollection) private fCDcomp : TCDComponent; protected function GetOwner:TPersistent; override; procedure Update(Item: TCollectionItem); override; public constructor create(CD:TCDComponent); function Add : TMP3Item; end;
unit unit4; interface uses Classes; type TMP3Item = Class(TCollectionItem) // protected public function GetDisplayName: string; override; public constructor Create(Collection: TCollection); override; end;
GetDisplayName
publique
pour que l'on puisse l'invoquer depuis les autres unités.Par contre, les unité 2 et 3 ne peuvent compiler à cause de cette fameuse référence circulaire !
Voici trois propositions pour contourner ce problème inhérent au Pascal :
Méthode 1, le sous-typage
Quand on regarde l'Interface de l'unité 3, on remarque qu'il suffit d'une toute petite modification pour qu'elle soit valide :unit unit3; interface uses Classes,{unit2,}unit4; type TMP3Collection = Class(Tcollection) private fCDcomp : TComponent; // TCDComponent; protected function GetOwner:TPersistent; override; procedure Update(Item: TCollectionItem); override; public constructor create(CD:TComponent{TCDComponent}); function Add : TMP3Item; end;
implementation procedure TMP3Collection.Update(Item: TCollectionItem); begin FCDComp.ListeModifiee(TMP3item(item)); end;
fCDComp
comme il le faut. Et pour s'assurer que le transtypage soit valide,
nous le testerons dès le départ :
implementation uses SysUtils,unit2; // référence à unit2 autorisée constructor TMP3Collection.create(CD:TComponent); begin // Validation du transtypage if not (CD is TCDComponent) then raise Exception.Create('TMP3Collection a besoin d''un TCDComponent'); inherited create(TMP3Item); fCDComp := CD; end; procedure TMP3Collection.Update(Item: TCollectionItem); begin // Transtypage valide TCDComponent(FCDComp).ListeModifiee(TMP3item(item)); end;
Méthode 2, l'ancètre virtuel
Si l'on veut éviter la "fausse" déclaration defCDComp
de la méthode 1, il est possible de placer dans une nouvelle unité,
un composant abstrait (ou pas) qui fera le lien entre les deux unités en conflit :
unit unit5; interface uses Classes,unit4; type TCustomCDComponent = Class(Tcomponent) public procedure ListeModifiee(Item:TMP3Item); virtual; abstract; end; implementation end.
unit unit2; interface uses Classes,unit3,unit4,unit5; type TCDComponent = Class(TCustomCDComponent) private fItems : TMP3Collection; public // surcharge de la méthode de TCustomCDComponent procedure ListeModifiee(Item:TMP3Item); override; end;
unit unit3; interface uses Classes,unit4,unit5; // aucune référence à unit2 et TCDComponent !! type TMP3Collection = Class(Tcollection) private fCDcomp : TCustomCDComponent; protected function GetOwner:TPersistent; override; procedure Update(Item: TCollectionItem); override; public constructor create(CD:TCustomCDComponent); function Add : TMP3Item; end;
Méthode 3, l'interface
La déclaration de classes virtuelles abstraites est parfois lourde, et pire encore, elle oblige la classeTCDComponent
à dériver d'elle.
Dans notre exemple ce n'est pas grave, mais on aurait pu avoir à dériver TCDComponent
d'une classe TAdvComponent
...Pour éviter ces problèmes, il existe une solution élégante avec les Interfaces :
unit unit2; interface uses Classes,unit3,unit4,unit5; type TCDComponent = Class(TComponent,ICDComponent) private fItems : TMP3Collection; public // implémente ICDComponent function GetInstance:TPersistent; procedure ListeModifiee(Item:TMP3Item); end; implementation function TCDComponent.GetInstance:TPersistent; begin Result:=Self; end; ...
unit unit3; interface uses Classes,unit4,unit5; // aucune référence à unit2 et TCDComponent !! type TMP3Collection = Class(Tcollection) private fCDcomp : ICDComponent; protected function GetOwner:TPersistent; override; procedure Update(Item: TCollectionItem); override; public constructor create(CD:ICDComponent); function Add : TMP3Item; end; implementation function GetOwner:TPersistent; begin Result:=fCDComp.GetInstance; end; ...
unit unit5; interface uses Classes,unit4; type ICDComponent = Interface public function GetInstance:TPersistent; procedure ListeModifiee(Item:TMP3Item); end; implementation end.
GetInstance
à ICDComponent
car TMP3Collection
à besoin de connaitre
l'instance de la classe. Si vous testez ce code, vous remarquerez également dans le constructor qu'il faut renseigner
fCDComp
avant d'appeler le constructeur hérité sous peine d'erreur dans GetOwner
(qui ne renvoie
pas nil comme avant, mais provoque une erreur lors de l'appel à GetInstance
)
Pour finir, et pour étudier tout cela à tête reposée, vous pouvez télécharger ici le code source de ces exemples.