Fonctionnement du framework tiOPF pour delphi / lazarus. Modèle de visiteur

Du traducteur


Il y a deux raisons pour lesquelles j'ai entrepris de traduire plusieurs documents sur le framework développé il y a vingt ans pour l'environnement de programmation peu populaire:

1. Il y a quelques années, après avoir appris les nombreux avantages de travailler avec Entity Framework en tant qu'ORM pour la plate-forme .Net, j'ai cherché en vain des analogues pour l'environnement Lazarus et, en général, pour freepascal.
Étonnamment, il manque pour elle de bons ORM. Tout ce qui a été trouvé à l'époque était un projet open-source appelé tiOPF , développé à la fin des années 90 pour delphi, puis porté sur freepascal. Cependant, ce cadre est fondamentalement différent de l'aspect habituel des ORM grands et épais.

Il n'y a pas de méthodes visuelles pour concevoir des objets (dans Entity - modèle d'abord) et mapper des objets aux champs de tables dans une base de données relationnelle (dans Entity - base de données d'abord) dans tiOPF. Le développeur lui-même positionne ce fait comme l'une des lacunes du projet, mais comme mérite, il offre une orientation complète spécifiquement sur le modèle d'entreprise objet, cela ne vaut qu'un code dur ...

C'est au niveau du codage en dur proposé que j'ai eu des problèmes. À cette époque, je ne connaissais pas très bien ces paradigmes et méthodes que le développeur de framework utilisait dans son intégralité et mentionnés dans la documentation plusieurs fois par paragraphe (modèles de conception du visiteur, éditeur de liens, observateur, plusieurs niveaux d'abstraction pour l'indépendance du SGBD, etc. .). Mon grand projet de travail avec la base de données à l'époque était entièrement axé sur les composants visuels de Lazarus et la façon de travailler avec les bases de données offertes par l'environnement visuel, par conséquent, des tonnes du même code: trois tables dans la base de données elle-même avec presque la même structure et des données homogènes, trois formulaires identiques pour la visualisation, trois formulaires identiques pour l'édition, trois formulaires identiques pour les rapports et tout le reste du haut de la rubrique «Comment ne pas concevoir de logiciel».

Après avoir lu suffisamment de littérature sur les principes de la conception correcte des bases de données et des systèmes d'information, y compris l'étude des modèles, ainsi que la connaissance de l'Entity Framework, j'ai décidé de refactoriser complètement la base de données elle-même et mon application. Et si j'ai complètement fait face à la première tâche, alors pour la mise en œuvre de la seconde, il y avait deux routes allant dans des directions différentes: soit aller complètement à l'étude .net, C # et l'Entity Framework, soit trouver un ORM approprié pour le système Lazarus familier. Il y avait aussi une troisième, première piste cyclable discrète - pour écrire l'ORM en fonction de vos besoins, mais ce n'est pas le cas maintenant.

Le code source du framework n'est pas beaucoup commenté, mais les développeurs ont néanmoins préparé (apparemment dans la période initiale de développement) une certaine quantité de documentation. Tout cela, bien sûr, est anglophone, et l'expérience montre que, malgré l'abondance de code, de diagrammes et de phrases de programmation de modèles, de nombreux programmeurs russophones sont encore mal orientés dans la documentation en anglais. Pas toujours et tout le monde n'a pas envie d'apprendre à comprendre le texte technique anglais sans avoir besoin de l'esprit pour le traduire en russe.

De plus, la relecture répétée du texte à traduire vous permet de voir ce que j'ai manqué lorsque j'ai rencontré la documentation pour la première fois, je ne l'ai pas comprise complètement ou correctement. C'est-à-dire que c'est pour lui l'occasion de mieux connaître le cadre à l'étude.

2. Dans la documentation, l'auteur saute intentionnellement ou non certains morceaux de code, probablement évidents à son avis. En raison de la limitation de son écriture, la documentation utilise des mécanismes et des objets obsolètes comme exemples, supprimés ou plus utilisés dans les nouvelles versions du framework (mais n'ai-je pas dit qu'il continue de se développer lui-même?). De plus, lorsque j'ai répété moi-même les exemples développés, j'ai trouvé des erreurs qui devraient être corrigées. Par conséquent, par endroits, je me suis permis non seulement de traduire le texte, mais aussi de le compléter ou de le réviser afin qu'il reste pertinent et que les exemples fonctionnent.

Je veux commencer la traduction de documents à partir d'un article de Peter Henrikson sur la première "baleine" sur laquelle repose tout le cadre - le modèle de visiteur. Texte original publié ici .

Modèle de visiteur et tiOPF


Le but de cet article est d'introduire le modèle Visitor, dont l'utilisation est l'un des principaux concepts du framework tiOPF (TechInsite Object Persistence Framework). Nous examinerons le problème en détail, après avoir analysé des solutions alternatives avant d'utiliser le Visitor. Dans le processus de développement de notre propre concept de visiteur, nous devrons faire face à un autre défi: la nécessité de parcourir tous les objets de la collection. Cette question sera également étudiée.

La tâche principale consiste à proposer une méthode généralisée pour effectuer un ensemble de méthodes associées sur certains objets de la collection. Les méthodes effectuées peuvent varier en fonction de l'état interne des objets. Nous ne pouvons pas exécuter de méthodes du tout, mais nous pouvons exécuter plusieurs méthodes sur les mêmes objets.

Le niveau de formation nécessaire


Le lecteur doit être familier avec le pascal objet et maîtriser les principes de base de la programmation orientée objet.

Exemple de tâche métier dans cet article


À titre d'exemple, nous développerons un carnet d'adresses qui vous permettra de créer des enregistrements de personnes et de leurs coordonnées. Avec l'augmentation des moyens de communication possibles entre les personnes, l'application devrait vous permettre de manière flexible d'ajouter de telles méthodes sans traitement de code significatif (je me souviens une fois avoir fini de traiter le code pour ajouter un numéro de téléphone, j'ai immédiatement dû le traiter à nouveau pour ajouter un e-mail). Nous devons fournir deux catégories d'adresses: réelles, telles que l'adresse personnelle, postale, professionnelle et électronique: fixe, fax, mobile, e-mail, site Web.

Au niveau de la présentation, notre application devrait ressembler à Explorer / Outlook, c'est-à-dire qu'elle est censée utiliser des composants standard tels que TreeView et ListView. L'application doit fonctionner rapidement et ne pas donner l'impression d'un logiciel client-serveur encombrant.

Une application pourrait ressembler Ă  ceci:



Dans le menu contextuel de l'arborescence, vous pouvez choisir d'ajouter / supprimer le contact d'une personne ou d'une entreprise, et cliquez avec le bouton droit sur la liste des données de contact pour appeler leur boîte de dialogue d'édition, supprimer ou ajouter des données.

Les données peuvent être enregistrées sous différentes formes, et à l'avenir, nous examinerons comment utiliser ce modèle pour implémenter cette fonctionnalité.

Avant de commencer


Nous commencerons par une simple collection d'objets - une liste de personnes qui à leur tour ont deux propriétés - nom (Nom) et adresse (EmailAdrs). Pour commencer, la liste sera remplie de données dans le constructeur, puis elle sera chargée à partir d'un fichier ou d'une base de données. Bien sûr, il s'agit d'un exemple très simplifié, mais il suffit de mettre pleinement en œuvre le modèle Visitor.

Créez une nouvelle application et ajoutez deux classes de la section interface du module principal: TPersonList (hérité de TObjectList et nécessite un plug-in dans le module contnrs) et TPerson (hérité de TObject):

TPersonList = class(TObjectList)  public    constructor Create;  end;  TPerson = class(TObject)  private    FEMailAdrs: string;    FName: string;  public    property Name: string read FName write FName;    property EMailAdrs: string read FEMailAdrs write FEMailAdrs;  end; 

Dans le constructeur TPersonList, nous créons trois objets TPerson et les ajoutons à la liste:

 constructor TPersonList.Create; var lData: TPerson; begin inherited; lData := TPerson.Create; lData.Name := 'Malcolm Groves'; lData.EMailAdrs := 'malcolm@dontspamme.com';  // (ADUG Vice President) Add(lData); lData := TPerson.Create; lData.Name := 'Don MacRae';  // (ADUG President) lData.EMailAdrs := 'don@dontspamme.com'; Add(lData); lData := TPerson.Create; lData.Name := 'Peter Hinrichsen';  // (Yours truly) lData.EMailAdrs := 'peter_hinrichsen@dontspamme.com'; Add(lData); end; 

Tout d'abord, nous allons parcourir la liste et effectuer deux opérations sur chaque élément de la liste. Les opérations sont similaires, mais pas les mêmes: un simple appel ShowMessage pour afficher le contenu des propriétés Name et EmailAdrs des objets TPerson. Ajoutez deux boutons au formulaire et nommez-les comme ceci:



Dans la portée préférée de votre formulaire, vous devez également ajouter une propriété (ou simplement un champ) FPersonList de type TPersonList (si le type est déclaré sous le formulaire, modifiez l'ordre ou faites une déclaration de type préliminaire), et appelez le constructeur dans le gestionnaire d'événement onCreate:

 FPersonList := TPersonList.Create; 

Pour libérer correctement la mémoire dans le gestionnaire d'événements onClose du formulaire, cet objet doit être détruit:

 FPersonList.Free. 

Étape 1. Itération du code dur


Pour afficher les noms des objets TPerson, ajoutez le code suivant au gestionnaire d'événements onClick du premier bouton:

 procedure TForm1.Button1Click(Sender: TObject); var i: integer; begin for i := 0 to FPersonList.Count - 1 do   ShowMessage(TPerson(FPersonList.Items[i]).Name); end; 

Pour le deuxième bouton, le code du gestionnaire sera le suivant:

 procedure TForm1.Button2Click(Sender: TObject); var i: integer; begin for i := 0 to FPersonList.Count - 1 do   ShowMessage(TPerson(FPersonList.Items[i]).EMailAdrs); end; 

Voici les bancs évidents de ce code:

  • deux mĂ©thodes qui font presque la mĂŞme chose. Toute la diffĂ©rence rĂ©side uniquement dans le nom de la propriĂ©tĂ© de l'objet qu'ils montrent;
  • l'itĂ©ration sera fastidieuse, surtout lorsque vous ĂŞtes obligĂ© d'Ă©crire une boucle similaire Ă  cent endroits dans le code;
  • un casting dur pour TPerson est semĂ© de situations exceptionnelles. Que faire s'il y a une instance de TAnimal dans la liste sans propriĂ©tĂ© d'adresse? Il n'y a aucun mĂ©canisme pour arrĂŞter l'erreur et se dĂ©fendre contre elle dans ce code.

Voyons comment améliorer le code en introduisant une abstraction: nous passons le code de l'itérateur à la classe parente.

Étape 2. Résumé de l'itérateur


Donc, nous voulons déplacer la logique de l'itérateur vers la classe de base. L'itérateur de liste lui-même est très simple:

 for i := 0 to FList.Count - 1 do // -    … 

Il semble que nous prévoyons d'utiliser un modèle Iterator . Du livre sur le livre des modèles de conception de Gang-of-Four , il est connu que l'itérateur peut être externe et interne. Lors de l'utilisation d'un itérateur externe, le client contrôle explicitement le parcours en appelant la méthode Next (par exemple, l'énumération des éléments TCollection est contrôlée par les méthodes First, Next, Last). Nous utiliserons ici l'itérateur interne, car il est plus facile d'implémenter la traversée d'arbre avec son aide, ce qui est notre objectif. Nous allons ajouter la méthode Iterate à notre classe de liste et lui passer une méthode de rappel, qui doit être effectuée sur chaque élément de la liste. Le rappel dans l'objet pascal est déclaré comme un type procédural, nous aurons, par exemple, TDoSomethingToAPerson.

Ainsi, nous déclarons un type procédural TDoSomethingToAPerson, qui prend un paramètre de type TPerson. Le type procédural vous permet d'utiliser la méthode comme paramètre d'une autre méthode, c'est-à-dire d'implémenter le rappel. De cette façon, nous allons créer deux méthodes, dont l'une affichera la propriété Name de l'objet, et l'autre - la propriété EmailAdrs, et elles seront elles-mêmes transmises en tant que paramètre à l'itérateur général. Enfin, la section de déclaration de type devrait ressembler à ceci:

 { TPerson } TPerson = class(TObject) private   FEMailAdrs: string;   FName: string; public   property Name: string read FName write FName;   property EMailAdrs: string read FEMailAdrs write FEMailAdrs; end; TDoSomethingToAPerson = procedure(const pData: TPerson) of object; { TPersonList } TPersonList = class(TObjectList) public   constructor Create;   procedure   DoSomething(pMethod: TDoSomethingToAPerson); end;   DoSomething: procedure TPersonList.DoSomething(pMethod: TDoSomethingToAPerson); var i: integer; begin for i := 0 to Count - 1 do   pMethod(TPerson(Items[i])); end; 

Maintenant, pour effectuer les actions nécessaires sur les éléments de la liste, nous devons faire deux choses. Premièrement, définissez les opérations nécessaires à l'aide de méthodes dont la signature est spécifiée par TDoSomethingToAPerson, et deuxièmement, écrivez des appels DoSomething avec les pointeurs vers ces méthodes passées en paramètre. Dans la section de description du formulaire, ajoutez deux déclarations:

 private   FPersonList: TPersonList;   procedure DoShowName(const pData: TPerson);   procedure DoShowEmail(const pData: TPerson); 

Dans la mise en œuvre de ces méthodes, nous indiquons:

 procedure TForm1.DoShowName(const pData: TPerson); begin ShowMessage(pData.Name); end; procedure TForm1.DoShowEmail(const pData: TPerson); begin ShowMessage(pData.EMailAdrs); end; 

Le code des gestionnaires de boutons est modifié comme suit:

 procedure TForm1.Button1Click(Sender: TObject); begin FPersonList.DoSomething(@DoShowName); end; procedure TForm1.Button2Click(Sender: TObject); begin FPersonList.DoSomething(@DoShowEmail); end; 

Déjà mieux. Nous avons maintenant trois niveaux d'abstractions dans notre code. Un itérateur générique est une méthode de classe qui implémente une collection d'objets. La logique métier (jusqu'à présent, la sortie de messages sans fin via ShowMessage) est placée séparément. Au niveau de la présentation (interface graphique), la logique métier est appelée sur une seule ligne.

Il est facile d'imaginer comment un appel à ShowMessage peut être remplacé par du code qui enregistre nos données de TPerson dans une base de données relationnelle à l'aide de la requête SQL de l'objet TQuery. Par exemple, comme ceci:

 procedure TForm1.SavePerson(const pData: TPerson); var lQuery: TQuery; begin lQuery := TQuery.Create(nil); try   lQuery.SQL.Text := 'insert into people values (:Name, :EMailAdrs)';   lQuery.ParamByName('Name').AsString := pData.Name;   lQuery.ParamByName('EMailAdrs').AsString := pData.EMailAdrs;   lQuery.Datababase := gAppDatabase;   lQuery.ExecSQL; finally   lQuery.Free; end; end; 

Soit dit en passant, cela introduit un nouveau problème de maintien d'une connexion à la base de données. Dans notre demande, la connexion à la base de données est effectuée via un objet gAppDatabase global. Mais où sera-t-il situé et comment fonctionner? De plus, nous sommes tourmentés à chaque étape de l'itérateur pour créer des objets TQuery, configurer la connexion, exécuter la requête et ne pas oublier de libérer la mémoire. Il serait préférable d'encapsuler ce code dans une classe qui encapsule la logique de création et d'exécution de requêtes SQL, ainsi que de configuration et de maintenance d'une connexion à la base de données.

Étape 3. Passer un objet au lieu de passer un pointeur vers un rappel


Passer l'objet à la méthode itérateur de la classe de base résoudra le problème de la maintenance de l'état. Nous allons créer une classe visiteur abstraite TPersonVisitor avec une seule méthode Execute et passer l'objet à cette méthode en tant que paramètre. L'interface abstraite du visiteur est présentée ci-dessous:

   TPersonVisitor = class(TObject) public   procedure Execute(pPerson: TPerson); virtual; abstract; end; 

Ensuite, ajoutez la méthode Iterate à notre classe TPersonList:

 TPersonList = class(TObjectList) public   constructor Create;   procedure Iterate(pVisitor: TPersonVisitor); end; 

La mise en œuvre de cette méthode sera la suivante:

 procedure TPersonList.Iterate(pVisitor: TPersonVisitor); var i: integer; begin for i := 0 to Count - 1 do   pVisitor.Execute(TPerson(Items[i])); end; 

Un objet du Visitor implémenté de la classe TPersonVisitor est passé à la méthode Iterate, et lors de l'itération à travers les éléments de liste pour chacun d'eux, le Visitor spécifié (sa méthode d'exécution) est appelé avec l'instance de TPerson comme paramètre.

Créons deux implémentations de Visitor - TShowNameVisitor et TShowEmailVistor, qui effectueront le travail requis. Voici comment reconstituer la section des interfaces du module:

 { TShowNameVisitor } TShowNameVisitor = class(TPersonVisitor) public   procedure Execute(pPerson: TPerson); override; end; { TShowEmailVisitor } TShowEmailVisitor = class(TPersonVisitor) public   procedure Execute(pPerson: TPerson); override; end; 

Par souci de simplicité, l'implémentation des méthodes d'exécution sur celles-ci sera toujours une seule ligne - ShowMessage (pPerson.Name) et ShowMessage (pPerson.EMailAdrs).

Et changez le code des gestionnaires de clic de bouton:

 procedure TForm1.Button1Click(Sender: TObject); var lVis: TPersonVisitor; begin lVis := TShowNameVisitor.Create; try   FPersonList.Iterate(lVis); finally   lVis.Free; end; end; procedure TForm1.Button2Click(Sender: TObject); var lVis: TPersonVisitor; begin lVis := TShowEmailVisitor.Create; try   FPersonList.Iterate(lVis); finally   lVis.Free; end; end; 

Maintenant, après avoir résolu un problème, nous en avons créé un autre pour nous-mêmes. La logique d'itérateur est encapsulée dans une classe distincte; les opérations effectuées pendant l'itération sont encapsulées dans des objets, ce qui nous permet d'enregistrer des informations sur l'état, mais la taille du code est passée d'une ligne (FPersonList.DoSomething (@DoShowName); à neuf lignes pour chaque gestionnaire de boutons. Maintenant, cela va nous aider - c'est le gestionnaire des visiteurs, qui se chargera de créer et de libérer leurs copies.Potentiellement, nous pouvons prévoir plusieurs opérations avec des objets pendant l'itération, pour cela le gestionnaire des visiteurs stockera leur liste et la parcourra à chaque étape, vous . Olnyaya que les opérations sélectionnées suivant démontrera clairement les avantages de cette approche, nous utiliserons les visiteurs pour enregistrer les données dans une base de données relationnelle en tant que données simple opération d'économie peuvent être effectuées par trois opérateurs différents SQL: CREATE, DELETE et UPDATE.

Étape 4. Encapsulation supplémentaire du visiteur


Avant de continuer, il faut encapsuler la logique du travail du Visiteur, le séparer de la logique métier de l'application pour qu'il n'y revienne pas. Pour ce faire, il nous faudra trois étapes: créer les classes de base TVisited et TVisitor, puis les classes de base pour l'objet métier et la collection d'objets métier, puis ajuster légèrement nos classes spécifiques TPerson et TPersonList (ou TPeople) afin qu'elles deviennent héritières de la base créée. cours. De manière générale, la structure des classes correspondra à un tel schéma:



L'objet TVisitor implémente deux méthodes: la fonction AcceptVisitor et la procédure Execute, dans laquelle l'objet de type TVisited est transmis. L'objet TVisited, à son tour, implémente la méthode Iterate avec un paramètre de type TVisitor. Autrement dit, TVisited.Iterate doit appeler la méthode Execute sur l'objet TVisitor transféré, en envoyant un lien vers sa propre instance en tant que paramètre, et si l'instance est une collection, la méthode Execute est appelée pour chaque élément de la collection. La fonction AcceptVisitor est nécessaire car nous développons un système généralisé. Il sera possible de passer au visiteur, qui fonctionne uniquement avec les types TPerson, une instance de la classe TDog, par exemple, et il doit y avoir un mécanisme pour empêcher les exceptions et les erreurs d'accès dues à une incompatibilité de type. La classe TVisited est la descendante de la classe TPersistent, car un peu plus tard nous devrons implémenter des fonctions liées à l'utilisation de RTTI.

La partie interface du module sera désormais la suivante:

 TVisited = class; { TVisitor } TVisitor = class(TObject) protected   function AcceptVisitor(pVisited: TVisited): boolean; virtual; abstract; public   procedure Execute(pVisited: TVisited); virtual; abstract; end; { TVisited } TVisited = class(TPersistent) public   procedure Iterate(pVisitor: TVisitor); virtual; end; 

Les méthodes de la classe abstraite TVisitor seront mises en œuvre par les héritiers, et la mise en œuvre générale de la méthode Iterate pour TVisited est donnée ci-dessous:

 procedure TVisited.Iterate(pVisitor: TVisitor); begin pVisitor.Execute(self); end; 

Dans le même temps, la méthode est déclarée virtuelle pour la possibilité de sa dérogation chez les héritiers.

Étape 5. Créez un objet métier et une collection partagés


Notre framework a besoin de deux classes de base supplémentaires: pour définir un objet métier et une collection de ces objets. Appelez-les TtiObject et TtiObjectList. L'interface du premier d'entre eux:

 TtiObject = class(TVisited) public   constructor Create; virtual; end; 

Plus tard dans le processus de développement, nous compliquerons cette classe, mais pour la tâche en cours, un seul constructeur virtuel avec la possibilité de la surcharger chez les héritiers suffit.

Nous prévoyons de générer la classe TtiObjectList à partir de TVisited afin d'utiliser le comportement dans les méthodes qui ont déjà été implémentées par l'ancêtre (il y a aussi d'autres raisons pour cet héritage qui seront discutées à sa place). De plus, rien n'interdit l'utilisation d' interfaces (interfaces) au lieu de classes abstraites.

La partie interface de la classe TtiObjectList sera la suivante:

 TtiObjectList = class(TtiObject) private   FList: TObjectList; public   constructor Create; override;   destructor Destroy; override;   procedure Clear;   procedure Iterate(pVisitor: TVisitor); override;   procedure Add(pData: TObject); end; 

Comme vous pouvez le voir, le conteneur lui-même avec les éléments objet est situé dans la section protégée et ne sera pas disponible pour les clients de cette classe. La partie la plus importante de la classe est l'implémentation de la méthode Iterate surchargée. Si dans la classe de base la méthode s'appelle simplement pVisitor.Execute (self), alors l'implémentation est liée à l'énumération de la liste:

 procedure TtiObjectList.Iterate(pVisitor: TVisitor); var i: integer; begin inherited Iterate(pVisitor); for i := 0 to FList.Count - 1 do   (FList.Items[i] as TVisited).Iterate(pVisitor); end; 

L'implémentation d'autres méthodes de classe prend une ligne de code sans tenir compte des expressions héritées placées automatiquement:

 Create: FList := TObjectList.Create; Destroy: FList.Free; Clear: if Assigned(FList) then FList.Clear; Add: if Assigned(FList) then FList.Add(pData); 

Il s'agit d'une partie importante de l'ensemble du système. Nous avons deux classes de base de logique métier: TtiObject et TtiObjectList. Les deux ont une méthode Iterate à laquelle une instance de la classe TVisited est passée. L'itérateur lui-même appelle la méthode Execute de la classe TVisitor et lui transmet une référence à l'objet lui-même. Cet appel est prédéfini dans le comportement de classe au niveau supérieur de l'héritage. Pour une classe de conteneur, chaque objet stocké dans la liste a également sa méthode Iterate, appelée avec un paramètre de type TVisitor, c'est-à-dire qu'il est garanti que chaque visiteur spécifique contournera tous les objets stockés dans la liste, ainsi que la liste elle-même en tant qu'objet conteneur.

Étape 6. Création d'un gestionnaire de visiteurs


Revenons donc au problème que nous avons nous-mêmes tiré de la troisième étape. Comme nous ne voulons pas créer et détruire à chaque fois des copies des Visiteurs, le développement du Manager sera la solution. Il doit effectuer deux tâches principales: gérer la liste des visiteurs (qui sont enregistrés comme tels dans la section d'initialisation des modules individuels) et les exécuter lorsqu'ils reçoivent la commande appropriée du client.
Pour implémenter le gestionnaire, nous compléterons notre module avec trois classes supplémentaires: le TVisClassRef, TVisMapping et TtiVisitorManager.

 TVisClassRef = class of TVisitor; 

TVisClassRef est un type de référence et indique le nom d'une classe particulière - un descendant de TVisitor. La signification de l'utilisation d'un type de référence est la suivante: lorsque la méthode de base Execute avec une signature est appelée

 procedure Execute(const pData: TVisited; const pVisClass: TVisClassRef), 

en interne, cette méthode peut utiliser une expression comme lVisitor: = pVisClass.Create pour créer une instance d'un Visiteur spécifique, sans d'abord connaître son type. Autrement dit, n'importe quelle classe - un descendant de TVisitor peut être créé dynamiquement à l'intérieur de la même méthode Execute lors du passage du nom de sa classe en tant que paramètre.

La deuxième classe, TVisMapping, est une structure de données simple avec deux propriétés: une référence au type TVisClassRef et une propriété de chaîne Command. Une classe est nécessaire pour comparer les opérations effectuées par leur nom (une commande, par exemple, «enregistrer») et la classe Visitor, que ces commandes exécutent. Ajoutez son code au projet:

 TVisMapping = class(TObject) private   FCommand: string;   FVisitorClass: TVisClassRef; public   property VisitorClass: TVisClassRef read FVisitorClass write FVisitorClass;   property Command: string read FCommand write FCommand; end; 

Et la dernière classe est TtiVisitorManager. Lorsque nous enregistrons le visiteur à l'aide du gestionnaire, une instance de la classe TVisMapping est créée, qui est entrée dans la liste des gestionnaires.
Ainsi, dans le Manager, une liste de Visiteurs est créée avec une commande de chaîne correspondante, à la réception de laquelle ils seront exécutés. L'interface de classe est ajoutée au module:

 TtiVisitorManager = class(TObject) private   FList: TObjectList; public   constructor Create;   destructor Destroy; override;   procedure RegisterVisitor(const pCommand: string; pVisitorClass: TVisClassRef);   procedure Execute(const pCommand: string; pData: TVisited); end; 

Ses principales méthodes sont RegisterVisitor et Execute. Le premier est généralement appelé dans la section d'initialisation du module, qui décrit la classe Visitor, et ressemble à ceci:

 initialization  gTIOPFManager.VisitorManager.RegisterVisitor('show', TShowNameVisitor);  gTIOPFManager.VisitorManager.RegisterVisitor('show', TShowEMailAdrsVisitor); 

Le code de la méthode elle-même sera le suivant:

 procedure TtiVisitorManager.RegisterVisitor(const pCommand: string; pVisitorClass: TVisClassRef); var lData: TVisMapping; begin lData := TVisMapping.Create; lData.Command := pCommand; lData.VisitorClass := pVisitorClass; FList.Add(lData); end; 

Il n'est pas difficile de remarquer que ce code est très similaire à l'implémentation Pascal du modèle Factory .

Une autre méthode Execute importante accepte deux paramètres: la commande par laquelle le visiteur ou son groupe à identifier sera identifié, ainsi que l'objet de données dont la méthode Iterate sera appelée avec un lien vers l'instance du visiteur souhaité. Le code complet de la méthode Execute est donné ci-dessous:

 procedure TtiVisitorManager.Execute(const pCommand: string; pData: TVisited); var i: integer; lVisitor: TVisitor; begin for i := 0 to FList.Count - 1 do   if SameText(pCommand, TVisMapping(FList.Items[i]).Command) then   begin     lVisitor := TVisMapping(FList.Items[i]).VisitorClass.Create;     try       pData.Iterate(lVisitor);     finally       lVisitor.Free;     end;   end; end; 

Ainsi, pour exécuter deux visiteurs précédemment enregistrés avec une seule équipe, nous avons besoin d'une seule ligne de code:

 gTIOPFManager.VisitorManager.Execute('show', FPeople); 

Ensuite, nous compléterons notre projet afin que vous puissiez appeler des commandes similaires:

 //      gTIOPFManager.VisitorManager.Execute('read', FPeople); //      gTIOPFManager.VisitorManager.Execute('save', FPeople). 

Étape 7. Ajustement des classes de logique métier


L'ajout de l'ancêtre des classes TtiObject et TtiObjectList pour nos objets métier TPerson et TPeople nous permettra d'encapsuler la logique de l'itérateur dans la classe de base et de ne plus la toucher.En outre, il devient possible de transférer des objets avec des données vers le gestionnaire de visiteurs.

La nouvelle déclaration de classe de conteneur ressemblera à ceci:

 TPeople = class(TtiObjectList); 

En fait, la classe TPeople n'a même rien à implémenter. Théoriquement, nous pourrions nous passer d'une déclaration TPeople et stocker des objets dans une instance de la classe TtiObjectList, mais comme nous prévoyons d'écrire des visiteurs ne traitant que des instances TPeople, nous avons besoin de cette classe. Dans la fonction AcceptVisitor, les vérifications suivantes seront effectuées:

 Result := pVisited is TPeople. 

Pour la classe TPerson, nous ajoutons l'ancêtre TtiObject et déplaçons les deux propriétés existantes dans la portée publiée, car à l'avenir, nous devrons utiliser RTTI avec ces propriétés. C'est cela beaucoup plus tard qui réduira considérablement le code impliqué dans le mappage des objets et des enregistrements dans une base de données relationnelle:

 TPerson = class(TtiObject) private   FEMailAdrs: string;   FName: string; published   property Name: string read FName write FName;   property EMailAdrs: string read FEMailAdrs write FEMailAdrs; end; 

Étape 8. Créez une vue prototype


Remarque . Dans l'article d'origine, l'interface graphique était basée sur les composants que l'auteur de tiOPF a créés pour la commodité de travailler avec son framework dans delphi. Il s'agissait d'analogues des composants DB Aware, qui étaient des contrôles standard tels que les étiquettes, les champs de saisie, les cases à cocher, la liste, etc., mais en même temps associés à certaines propriétés des objets tiObject ainsi qu'aux composants d'affichage de données associés aux champs des tables de base de données. Au fil du temps, l'auteur du framework a marqué les packages avec ces composants visuels comme obsolètes et indésirables à utiliser.En retour, il suggère de créer un lien entre les composants visuels et les propriétés de classe en utilisant le modèle de conception Mediator. Ce modèle est le deuxième plus important de toute l'architecture du framework. La description de l’intermédiaire par l’auteur fait l’objet d’un article distinct, comparable en volume à ce manuel, je vous propose donc ici ma version simplifiée en tant qu’interface graphique.

Renommez le bouton 1 du formulaire de projet en «afficher la commande», et le bouton 2 soit le laisser sans gestionnaire pour l'instant, soit le nommer immédiatement «enregistrer la commande». Jetez un composant mémo sur le formulaire et placez tous les éléments à votre goût.

Ajoutez une classe Visitor qui implémentera la commande show:

Interface -

 TShowVisitor = class(TVisitor) protected   function AcceptVisitor(pVisited: TVisited): boolean; override; public   procedure Execute(pVisited: TVisited); override; end; 

Et la mise en œuvre est -
 function TShowVisitor.AcceptVisitor(pVisited: TVisited): boolean; begin Result := (pVisited is TPerson); end; procedure TShowVisitor.Execute(pVisited: TVisited); begin if not AcceptVisitor(pVisited) then   exit; Form1.Memo1.Lines.Add(TPerson(pVisited).Name + ': ' + TPerson(pVisited).EMailAdrs); end; 

AcceptVisitor vérifie que l'objet transféré est une instance de TPerson, car le visiteur ne doit exécuter la commande qu'avec de tels objets. Si le type correspond, la commande est exécutée et une ligne avec les propriétés de l'objet est ajoutée au champ de texte.

Les actions de soutien à l'intégrité du code seront les suivantes. Ajoutez deux propriétés à la description du formulaire lui-même dans la section privée: FPeople de type TPeople et VM de type TtiVisitorManager. Dans le gestionnaire d'événements de création de formulaire, nous devons lancer ces propriétés, ainsi que d'enregistrer le visiteur avec la commande "show":

 FPeople := TPeople.Create; FillPeople; VM := TtiVisitorManager.Create; VM.RegisterVisitor('show',TShowVisitor); 

FilPeople est également une procédure auxiliaire remplissant une liste avec trois objets; son code est tiré du constructeur de liste précédent. N'oubliez pas de détruire tous les objets créés. Dans ce cas, nous écrivons FPeople.Free et VM.Free dans le gestionnaire de fermeture de formulaire.

Et maintenant - bams! - gestionnaire du premier bouton:

 Memo1.Clear; VM.Execute('show',FPeople); 

D'accord, tellement plus amusant. Et ne jurez pas sur le hachage de toutes les classes dans un module. À la toute fin du manuel, nous ratisserons ces décombres.

Étape 9. La classe de base du visiteur travaillant avec des fichiers texte


A ce stade, nous allons créer la classe de base du Visiteur qui sait travailler avec des fichiers texte. Il existe trois façons de travailler avec des fichiers dans l'objet pascal: les anciennes procédures à partir du premier pascal (comme AssignFile et ReadLn), le travail à travers les flux (TStringStream ou TFileStream) et l'utilisation de l'objet TStringList.

Si la première méthode est très dépassée, la deuxième et la troisième sont une bonne alternative basée sur la POO. Dans le même temps, travailler avec des flux offre en outre des avantages tels que la possibilité de compresser et de chiffrer des données, mais la lecture et l'écriture ligne par ligne dans un flux est une sorte de redondance dans notre exemple. Pour plus de simplicité, nous choisirons une TStringList, qui a deux méthodes simples - LoadFromFile et SaveToFile. Mais rappelez-vous qu'avec des fichiers volumineux, ces méthodes ralentiront considérablement, de sorte que le flux sera le choix optimal pour elles.

Interface de classe de base TVisFile:

 TVisFile = class(TVisitor) protected   FList: TStringList;   FFileName: TFileName; public   constructor Create; virtual;   destructor Destroy; override; end; 

Et l'implémentation du constructeur et du destructeur:

 constructor TVisFile.Create; begin inherited Create; FList := TStringList.Create; if FileExists(FFileName) then   FList.LoadFromFile(FFileName); end; destructor TVisFile.Destroy; begin FList.SaveToFile(FFileName); FList.Free; inherited; end; 

La valeur de la propriété FFileName sera affectée dans les constructeurs des descendants de cette classe de base (n'utilisez pas le codage en dur, que nous organiserons ici, comme style de programmation principal après!). Le diagramme des classes Visitor travaillant avec des fichiers est le suivant:



Conformément au diagramme ci-dessous, nous créons deux descendants de la classe de base TVisFile: TVisTXTFile et TVisCSVFile. L'un fonctionnera avec des fichiers * .csv dans lesquels les champs de données sont séparés par un symbole (virgule), le second - avec des fichiers texte dans lesquels les champs de données individuels auront une longueur fixe sur une ligne. Pour ces classes, nous redéfinissons uniquement les constructeurs comme suit:

 constructor TVisCSVFile.Create; begin FFileName := 'contacts.csv'; inherited Create; end; constructor TVisTXTFile.Create; begin FFileName := 'contacts.txt'; inherited Create; end. 

Étape 10. Ajoutez le gestionnaire de visiteurs des fichiers texte


Ici, nous ajouterons deux visiteurs spécifiques, l'un lira un fichier texte, le second y écrira. Le visiteur qui lit doit remplacer les méthodes de classe de base AcceptVisitor et Execute. AcceptVisitor vérifie que l'objet de classe TPeople est transmis au visiteur:

 Result := pVisited is TPeople; 

L'implémentation d'exécution est la suivante:

 procedure TVisTXtRead.Execute(pVisited: TVisited); var i: integer; lData: TPerson; begin if not AcceptVisitor(pVisited) then   Exit; //==> TPeople(pVisited).Clear; for i := 0 to FList.Count - 1 do begin   lData := TPerson.Create;   lData.Name := Trim(Copy(FList.Strings[i], 1, 20));   lData.EMailAdrs := Trim(Copy(FList.Strings[i], 21, 80));   TPeople(pVisited).Add(lData); end; end; 

Le visiteur efface d'abord la liste de l'objet TPeople qui lui est transmis par le paramètre, puis lit les lignes de son objet TStringList, dans lesquelles le contenu du fichier est chargé, crée un objet TPerson sur chaque ligne et l'ajoute à la liste des conteneurs TPeople. Par souci de simplicité, les propriétés name et emailadrs du fichier texte sont séparées par des espaces.

Le visiteur record implémente l'opération inverse. Son constructeur (surchargé) efface la TStringList interne (c'est-à-dire qu'il exécute l'opération FList.Clear; il est obligatoire après hérité), AcceptVisitor vérifie que l'objet de classe TPerson est passé, ce qui n'est pas une erreur, mais une différence importante par rapport à la même méthode de lecture Visitor. Il semblerait plus facile d'implémenter l'enregistrement de la même manière - scannez tous les objets conteneurs, ajoutez-les à une StringList puis enregistrez-le dans un fichier. Tout cela était le cas si nous parlions vraiment de l'écriture finale des données dans un fichier, mais nous prévoyons de mapper les données à une base de données relationnelle, cela devrait être rappelé. Et dans ce cas, nous devons exécuter le code SQL uniquement pour les objets qui ont été modifiés (créés, supprimés ou modifiés). C'est pourquoi avant que le visiteur n'effectue une opération sur l'objet,il doit vérifier la correspondance de son type:

 Result := pVisited is Tperson; 

La méthode d'exécution ajoute simplement à la StringList interne une chaîne formatée avec la règle spécifiée: d'abord, le contenu de la propriété name de l'objet passé, complété avec des espaces jusqu'à 20 caractères, puis le contenu de la propriété emaiadrs:

 procedure TVisTXTSave.Execute(pVisited: TVisited); begin if not AcceptVisitor(pVisited) then   exit; FList.Add(PadRight(TPerson(pVisited).Name,20)+PadRight(TPerson(pVisited).EMailAdrs,60)); end; 

Étape 11. Ajoutez le gestionnaire de visiteurs des fichiers CSV


Les visiteurs en lecture et en écriture sont similaires dans presque tous leurs collègues des classes TXT, à l'exception de la façon de formater la dernière ligne d'un fichier: dans la norme CSV, les valeurs des propriétés sont séparées par des virgules. Pour lire les lignes et les analyser en propriétés, nous utilisons la fonction ExtractDelimited du module strutils, et l'écriture est effectuée en concaténant simplement les lignes:

 procedure TVisCSVRead.Execute(pVisited: TVisited); var i: integer; lData: TPerson; begin if not AcceptVisitor(pVisited) then   exit; TPeople(pVisited).Clear; for i := 0 to FList.Count - 1 do begin   lData := TPerson.Create;   lData.Name := ExtractDelimited(1, FList.Strings[i], [',']);   lData.EMailAdrs := ExtractDelimited(2, FList.Strings[i], [',']);   TPeople(pVisited).Add(lData); end; end; procedure TVisCSVSave.Execute(pVisited: TVisited); begin if not AcceptVisitor(pVisited) then   exit; FList.Add(TPerson(pVisited).Name + ',' + TPerson(pVisited).EMailAdrs); end; 

Il ne nous reste plus qu'à enregistrer de nouveaux Visiteurs dans le Manager et à vérifier le fonctionnement de l'application. Dans le gestionnaire de création de formulaire, ajoutez le code suivant:

 VM.RegisterVisitor('readTXT', TVisTXTRead); VM.RegisterVisitor('saveTXT',TVisTXTSave); VM.RegisterVisitor('readCSV',TVisCSVRead); VM.RegisterVisitor('saveCSV',TVisCSVSave); 

Ancrez les boutons nécessaires sur le formulaire et affectez-leur les gestionnaires appropriés:



 procedure TForm1.ReadCSVbtnClick(Sender: TObject); begin VM.Execute('readCSV', FPeople); end; procedure TForm1.ReadTXTbtnClick(Sender: TObject); begin VM.Execute('readTXT', FPeople); end; procedure TForm1.SaveCSVbtnClick(Sender: TObject); begin VM.Execute('saveCSV', FPeople); end; procedure TForm1.SaveTXTbtnClick(Sender: TObject); begin VM.Execute('saveTXT', FPeople); end; 

Des formats de fichier supplémentaires pour l'enregistrement des données sont mis en œuvre en ajoutant simplement les visiteurs appropriés et en les enregistrant dans le gestionnaire. Et faites attention à ce qui suit: nous avons intentionnellement nommé les commandes différemment, c'est-à-dire, saveTXT et saveCSV. Si les deux visiteurs correspondent à une commande de sauvegarde, alors les deux commenceront sur la même commande, vérifiez-la vous-même.

Étape 12. Nettoyage final du code


Pour la plus grande beauté et pureté du code, ainsi que pour préparer un projet pour le développement ultérieur de l'interaction avec le SGBD, nous répartirons nos classes dans différents modules conformément à la logique et à leur objectif. En fin de compte, nous devrions avoir la structure suivante de modules dans le dossier du projet, ce qui nous permet de nous passer d'une relation circulaire entre eux (lors de l'assemblage, organisez les modules nécessaires dans les sections utilisations):

Module
Fonction
Cours
tivisitor.pas
Classes de base du modèle Visitor and Manager
TVisitor
TVisited
TVisMapping
TtiVisitorManager
tiobject.pas
Classes de logique métier de base
TtiObject
TtiObjectList
people_BOM.pas
Classes de logique métier spécifiques
TPerson
TPeople
people_SRV.pas
Des classes concrètes responsables de l'interaction
TVisFile
TVisTXTFile
TVisCSVFile
TVisCSVSave
TVisCSVRead
TVisTXTSave
TVisTXTRead

Conclusion


Dans cet article, nous avons examiné le problème de l'itération sur une collection ou une liste d'objets pouvant avoir différents types. Nous avons utilisé le modèle Visitor proposé par le GoF pour implémenter de manière optimale deux façons différentes de mapper des données à partir d'objets vers des fichiers de différents formats. Dans le même temps, différentes méthodes peuvent être mises en œuvre par une seule équipe grâce à la création du Visitor Manager. En fin de compte, les exemples simples et illustratifs discutés dans l'article nous aideront à développer davantage un système similaire pour mapper des objets à une base de données relationnelle.

Archive avec code source d'exemples - ici

Source: https://habr.com/ru/post/fr441266/


All Articles