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) ;D

Nous 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;
  
L'unité 4 ne pose aucun problème majeur, il nous faut simplement rendre la méthode 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;
En "sous-déclarant" fCDComp on brise la référence circulaire...mais cette modification interdit la compilation de la partie Implementation !
   implementation
   procedure TMP3Collection.Update(Item: TCollectionItem);
   begin

    FCDComp.ListeModifiee(TMP3item(item));
   end;
On peut cependant s'en sortie facilement en transtypant 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 de fCDComp 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.
 
pour mettre en oeuvre ce nouveau composant, il suffit de l'utiliser comme référence dans unit 2 et 3 :
  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 classe TCDComponent à 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.
  
Nous avons ajouté une méthode 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.