Developpez.com - Rubrique Delphi

Le Club des Développeurs et IT Pro

Implémententation du Singleton avec Delphi 7

Un tutoriel de Jérémy Laurent

Le 2015-01-28 21:00:10, par Alcatîz, Responsable Pascal, Lazarus et Assembleur
Implémentation du Singleton avec Delphi 7, par Jérémy Laurent
Premier article d'une série consacrée aux patrons de conception (design patterns)

Jérémy Laurent a décidé de présenter l'implémentation de différents patrons de conception (design patterns) avec Delphi. Le premier d'entre eux est le « Singleton », qui est un modèle visant à limiter l'instanciation d'une classe à un seul et unique objet. Il est couramment utilisé pour coordonner des opérations dans un système.

Cette implémentation ne pose aucun problème sur les dernières versions de Delphi ; par contre, avec les versions plus anciennes, les champs statiques ne sont pas autorisés et provoquent une erreur de compilation. Cet article présente différentes solutions.

http://jeremy-laurent.developpez.com...rns/singleton/

Qu'en pensez-vous ?
  Discussion forum
48 commentaires
  • Andnotor
    Rédacteur/Modérateur
    Envoyé par ALWEBER
    Lorsque l'objet défini contient des Class Var, Class Property, Class Function, etc. une instance unique de l'objet est automatiquement créée au lancement du programme.
    Non, il n'y a pas d'instance, c'est directement la déclaration de la classe qui est altérée.

    Envoyé par ALWEBER
    Même si les Class éléments peuvent cohabiter avec d'autres élément dans la description d'une classe, les Class éléments ne peuvent pas communiquer avec les autres éléments (et réciproquement).
    Le réciproquement est faux. L'instance sait où se trouve la déclaration de la classe. Tu l'illustres d'ailleurs dans func1, fv2 est bien incrémenté.

    Envoyé par ALWEBER
    En réalité Delphi travaille sur deux instances différentes de classes...
    Non

    Ton code n'est pas bon dans le sens où tu te réfères toujours à Self mais Self est l'instance dans un cas et la classe dans l'autre. Ajoute cette autre fonction...

    Code :
    1
    2
    3
    4
    5
    function TDemo.func2: string;
    begin
      v1 := integer(ClassType) ;
      result := intToHex (V1,8)
    end;
    ...et tu verras qu'elle renvoie bien le même pointeur que classfunc2.
  • Paul TOTH
    Expert éminent sénior
    Envoyé par popo
    Bonjour Paul et merci pour ta contribution.
    D'habitude, lorsque je lis un de tes posts, soit je suis d'accord, soit j'apprends quelque chose (cas qui arrive le plus souvent et je t'en remercie. ).
    Mais dans ce cas précis, à moins d'avoir loupé quelque chose, j'ai du mal à voir le singleton :
    - Il n'y a pas de classe.
    - Si je remplace l'entier par une classe dans ton exemple, rien ne m'empêche de faire un Free sur PublicValue
    - Toujours en remplaçant l'entier par une classe, rien ne m'empêche non plus de créer plusieurs instance puisque la déclaration reste publique.
    haha mais c'est l'unité qui est un singleton à elle seule il est impossible de l'instancier deux fois dans le projet, il est même impossible de la désallouer, justement car ce n'est pas un objet

    bon ok, tu ne peux pas dériver d'une classe de base (encore que tu peux très bien appeler une unité CustomSingleton.pas qui déclarerait une function MaMethod)...mais pas de méthodes dynamique (sauf à ajouter des pointeurs de fonctions...mais bon...) et surtout pas de property.

    en fait ce que je voulais surtout souligner c'est que Delphi n'est pas un langage 100% objet, et je ne vois aucun mal à utiliser des variables et fonctions globales qui présentent le gros avantage d'avoir implicitement un espace de nom lié à leur unité : "NomUnit.NomFonction" qui dans l'IDE se comportant très exactement comme "Instance.Methode"

    Et pour les points 2 et 3 de ta remarque, c'est tout aussi vrai d'un singleton déclaré comme ceci:
    Code :
    1
    2
    3
    4
    5
    6
    type
      TSingleton = class()
      public
        MaVar: Integer;
      end;
    la différence est la possibilité d'utiliser une "property" alors qu'avec une approche non objet tu auras au mieux des getter et setter.

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    interface // équivalent du public
    
    function GetVar: Integer;
    procedure SetVar(Value: Integer);
    
    implementation // équivalent du private
    
    var
      MaVar: Integer;
  • ShaiLeTroll
    Expert éminent sénior
    A la base le Tutoriel concerne "Singleton dans une version de Delphi qui ne supporte pas les champs statiques" comme l'est Delphi 7 !

    Evoquer XE est hors sujet comme le souligne AndNotOr !
    Avec le class var, créer un Singleton peut se faire comme en Java ou C#, ce n'est pas un grand défi !

    Sous Delphi 7, ce n'est pas difficile mais demande un peu plus d'astuce comme le démontre parfaitement le tutoriel de Popo

    D'ailleurs, le tutoriel évoque NewInstance pour gérer la création d'instance !

    Il existe plein de générique pour créer un Singleton en exemple sur le net,
    dont le mien [post=7517854]Design Pattern : Singleton - Découverte des Génériques et Class Constructor [/post] et ses classes TSLTSingleton et TSLTSingletonThreadSafe

    Ainsi pas besoin de réécrire le même code de gestion du singleton à chaque fois !
    Obligatoire sous D7 à cause de l'ancienneté de la version mais on peut faire mieux sous XE... !
  • Paul TOTH
    Expert éminent sénior
    allez, j'ajoute mes 2 cents, si on veux un vrai singleton il faudrait aussi une classe figée sinon rien ne m'empêche de surcharger ce qui est mis en place et sealed n'est pas supporté sous D7 si mon souvenir est bon.

    après moi j'aime bien cette écriture du singleton qui fonction avec toutes les versions

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    unit MySingleton;
    
    interface
    
    var
      PublicValue: Integer;
    
    procedure MaMethod;
    
    function GetPrivateValue: Integer;
    
    implementation
    
    var
      PrivateValue: Integer;
    
    function GetPrivateValue: Integer;
    begin
      Result := PrivateValue;
    end;
    
    procedure MaMethod;
    begin
    end;
    
    end.
    et ça s'utilise comme ceci:

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    uses
      MySingleton;
    
    begin
      MySingleton.PublicValue := 1;
      MySingleton.GetPrivateValie;
      MySingleton.MaMethod();
    end;
  • Andnotor
    Rédacteur/Modérateur
    Envoyé par ALWEBER
    Remarque sur la notion d'instance : J'ai choisi d'utiliser la notion d'instance plutôt que 'déclaration de la classe' car dans le cas qui est présenté la variable fV2 est bien localisée dans cet espace mémoire
    Oui mais c'est faux de parler d'instance. Une instance est une allocation dynamique en fonction d'une déclaration.

    Envoyé par ALWEBER
    Une première instance de classe est automatiquement créée au lancement du programme et sert de déclaration de la classe.
    Et comment cela serait-il possible ? Imagine simplement que le constructeur attende un certain nombre de paramètres, comment ceux-ci pourraient-ils être évalués ?
    La déclaration de la classe est en mémoire, oui ! Mais c'est le programme après tout ! Pour illustrer cela, je te propose une autre petit démo :

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    type
      TDemo = class
        procedure Test;
      end;
    
    var
      Demo :TDemo = nil;
    
    procedure TDemo.Test;
    begin
      ShowMessage('Texte');
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      Demo.Test;
    end;
    Il n'y a aucune instance de TDemo qui est créée, mais ça fonctionne ! Le code est là et exécutable et pourtant il n'y a pas de class var, class function, etc.

    Ajoutons maintenant une variable qui elle devrait être allouée (donc y avoir un Create) :
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    type
      TDemo = class
        Value :string;
        procedure Test;
      end;
    
    var
      Demo :TDemo = nil;
    
    procedure TDemo.Test;
    begin
      ShowMessage(Value);
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      Demo.Test;
    end;
    Nous obtenons une belle violation d'accès. Normal !
    Peut-on tester s'il y a bien eu allocation ? Oui, par Self :

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    type
      TDemo = class
        Value :string;
        procedure Test;
      end;
    
    var
      Demo :TDemo = nil;
    
    procedure TDemo.Test;
    begin
      if Self <> nil then
        ShowMessage(Value);
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      Demo.Test;
    end;
    Pas de ShowMessage, mais plus de VA

    En bref le code est toujours là et exécutable et ce n'est que Self (un pointeur sur les données propres à cette instance) qui est déterminant.

    Envoyé par ALWEBER
    Lorsque l'objet défini contient des Class Var, Class Property, Class Function, etc. L'accès à l'un de ces Class éléments ce fait en préfixant l'élement par le type de la classe.
    A nouveau c'est faux et à nouveau tu prouves le contraire dans ton exemple en appelant une fois TDemo.classfunc2 et l'autre aDemo.classfunc2. Mais puisque c'est une méthode de classe Self représente ici toujours la classe et non l'instance aDemo. La seule méthode de classe où Self représente l'instance est... le constructeur
  • ShaiLeTroll
    Expert éminent sénior
    Merci à la modération d'avoir déplacé ce message depuis Le patron de conception "Interpréteur" avec Delphi 7, un tutoriel de Jérémy Laurent


    Et que montre ce PDF ?
    Je ne vois pas de démonstration d'une instance fantôme, juste qu'il y a des "Champs de classe" et de l'autres les "champs d'instance";
    Evoquer une instance pour le stockage des "Champs de classe" est très maladroit et une erreur de première dans un cours.

    Et cela ne démontre aucunement une erreur sur le Singleton désigné plus haut, juste un choix d'avoir une instance unique au lieu d'utiliser uniquement des "Champs de classe".
    Avoir une instance d'ailleurs permet de passer à un Multiton (+Factory) de façon beaucoup plus transparente

    Sinon comme démontrer cette théorie de l'instance fantome avec l'héritage

    M est partagé par TDemo et TDemo2, l'un modifie l'autre

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    type
      TDemo = class
      private
        class var M: Integer;
      public
        class function IncM(): string;
      end;
    
      TDemo2 = class(TDemo)
      private
        class var K: Integer;
      public
        class function IncK(): string;
      end;
    
    
    
    { TDemo }
    
    class function TDemo.IncM(): string;
    begin
      Inc(M);
      Result := M.ToString();
    end;
    
    { TDemo2 }
    
    class function TDemo2.IncK(): string;
    begin
      Inc(M);
      Inc(K);
      Result := K.ToString();
    end;
    
    procedure TForm1.Button5Click(Sender: TObject);
    begin
      Memo.Lines.Add('M: ' + TDemo.IncM());
      Memo.Lines.Add('M: ' + TDemo.IncM());
      Memo.Lines.Add('M: ' + TDemo.IncM());
    
      Memo.Lines.Add('M: ' + TDemo2.IncM());
      Memo.Lines.Add('K: ' + TDemo2.IncK());
    
      Memo.Lines.Add('M: ' + TDemo2.IncM());
      Memo.Lines.Add('K: ' + TDemo2.IncK());
    
      Memo.Lines.Add('M: ' + TDemo2.IncM());
    end;
  • Andnotor
    Rédacteur/Modérateur
    Envoyé par ALWEBER
    Le générateur de Pattern dans Delphi XE pour le Singleton conduit maintenant à ce code :
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
      TSingleton = class
      strict private
        class var FInstance: TSingleton;
        constructor Create;
      public
        class function GetInstance: TSingleton;
      end;
    pour prévenir d'une éventuelle surcharge du constructor
    Ce code n'empêche absolument pas un TSingleton.Create mais c'est TObject.Create qui sera appelé uniquement. Quant à la surcharge, et bien c'est aussi TObject.Create qui le sera ! Il n'est pas possible de réduire la visibilité/portée d'une méthode/propriété.

    Quant au singleton, je me demande si on ne pourrait pas simplement l'écrire ainsi (et pardon à Jérémy d'avoir un peu dévié du sujet) :
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    type
      TSingleton = class
      strict private
        class var FInstance: TSingleton;
      public
        class function Get: TSingleton;
        constructor    Create;
        destructor     Destroy; override;
      end;
    
    constructor TSingleton.Create;
    var
      Ptr :pointer;
    begin
      if Assigned(FInstance) then
      begin
        //Libère l'objet en cours de création et
        //le remplace par l'instance existante
        Ptr := FInstance;
        Destroy;
        Self := Ptr;
      end;
    
      FInstance := Self;
    end;
    
    destructor TSingleton.Destroy;
    begin
      FInstance := nil;
      inherited;
    end;
    
    class function TSingleton.Get: TSingleton;
    begin
      Result := Create;
    end;
    ainsi appeler plusieurs fois le Create renverrait aussi l'instance existante. Après quelques essais, ça semble être le cas (et aucun Memory leak détecté).
  • popo
    Expert confirmé
    Envoyé par Andnotor
    Quant au singleton, je me demande si on ne pourrait pas simplement l'écrire ainsi (et pardon à Jérémy d'avoir un peu dévié du sujet)
    Il n'y a pas de mal.
    Envoyé par Andnotor

    :
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    type
      TSingleton = class
      strict private
        class var FInstance: TSingleton;
      public
        class function Get: TSingleton;
        constructor    Create;
        destructor     Destroy; override;
      end;
     
    constructor TSingleton.Create;
    var
      Ptr :pointer;
    begin
      if Assigned(FInstance) then
      begin
        //Libère l'objet en cours de création et
        //le remplace par l'instance existante
        Ptr := FInstance;
        Destroy;
        Self := Ptr;
      end;
     
      FInstance := Self;
    end;
     
    destructor TSingleton.Destroy;
    begin
      FInstance := nil;
      inherited;
    end;
     
    class function TSingleton.Get: TSingleton;
    begin
      Result := Create;
    end;
    ainsi appeler plusieurs fois le Create renverrait aussi l'instance existante. Après quelques essais, ça semble être le cas (et aucun Memory leak détecté).
    Je n'ai pas encore eu l'occasion de travailler avec XE. Si j'en ai le loisir, j'essaierai.

    Code :
    1
    2
    3
    Ptr := FInstance;
    Destroy;
    Self := Ptr;
    C'est assez bourrin comme méthode !
  • ShaiLeTroll
    Expert éminent sénior
    Intéressant l'article sur l'EDN
    C'est ni plus ni moins l'approche du TInterfacedObject mais gérer au niveau de la classe au lieu de l'instance via les variables globales
    le principal inconvénient de cette approche, c'est qu'il faut libérer explicitement le singleton après chaque utilisation pour faire baisser le compteur de référence
    il manque le ThreadSafe avec l'InterlockIncrement

    C'est la technique qu'exploite indirectement via une Factory (le Provider) dans la variante avancée à base de ISingleton où le compteur de référence est intégré au TInterfacedObject
    C'est le Provider qui force une vie "éternelle" en conservant une référence sur l'interface
  • Paul TOTH
    Expert éminent sénior
    Envoyé par popo
    C'est un concept que je vais avoir du mal à assimiler
    LOL, tu as toujours fait de l'objet ? car c'est juste de la déclaration de fonction classiques