Wie das tiOPF-Framework für Delphi / Lazarus funktioniert. Besuchervorlage

Vom Übersetzer


Es gibt zwei Gründe, warum ich mich verpflichtet habe, mehrere Materialien zu dem vor zwanzig Jahren für die nicht sehr beliebte Programmierumgebung entwickelten Framework zu übersetzen:

1. Nachdem ich vor einigen Jahren viele der Freuden der Arbeit mit dem Entity Framework als ORM für die .Net-Plattform kennengelernt hatte, suchte ich vergeblich nach Analoga für die Lazarus-Umgebung und im Allgemeinen nach Freepascal.
Überraschenderweise fehlen ihr gute ORMs. Alles, was damals gefunden wurde, war ein Open-Source-Projekt namens tiOPF , das Ende der 90er Jahre für Delphi entwickelt und später auf Freepascal portiert wurde. Dieses Framework unterscheidet sich jedoch grundlegend vom üblichen Aussehen großer und dicker ORMs.

Es gibt keine visuellen Möglichkeiten, Objekte (in Entity - Modell zuerst) zu entwerfen und Objekte Feldern in relationalen Datenbanktabellen (in Entität - Datenbank zuerst) in tiOPF zuzuordnen. Der Entwickler selbst positioniert diese Tatsache als einen der Mängel des Projekts. Als Verdienst bietet er jedoch eine vollständige Orientierung speziell auf das Geschäftsmodell des Objekts, es ist nur einen Hardcode wert ...

Auf der Ebene der vorgeschlagenen Hardcodierung hatte ich Probleme. Zu dieser Zeit war ich nicht sehr gut mit den Paradigmen und Methoden vertraut, die der Framework-Entwickler vollständig verwendete und in der Dokumentation mehrmals pro Absatz erwähnte (Entwurfsmuster des Besuchers, Linkers, Beobachters, verschiedene Abstraktionsebenen für die Unabhängigkeit von DBMS usw.). .). Mein großes Projekt, das zu dieser Zeit mit der Datenbank arbeitete, konzentrierte sich vollständig auf die visuellen Komponenten von Lazarus und die Art und Weise, mit Datenbanken zu arbeiten, die von der visuellen Umgebung angeboten werden. Das Ergebnis waren Tonnen desselben Codes: drei Tabellen in der Datenbank selbst mit fast derselben Struktur und homogenen Daten, Drei identische Formulare zum Anzeigen, drei identische Formulare zum Bearbeiten, drei identische Formulare für Berichte und alles andere oben in der Überschrift „So entwerfen Sie keine Software“.

Nachdem ich genügend Literatur zu den Prinzipien des korrekten Entwurfs von Datenbanken und Informationssystemen, einschließlich des Studiums von Vorlagen, gelesen und das Entity Framework kennengelernt hatte, entschied ich mich für ein vollständiges Refactoring sowohl der Datenbank selbst als auch meiner Anwendung. Und wenn ich die erste Aufgabe vollständig bewältigt habe, gab es für die Implementierung der zweiten zwei Wege, die in verschiedene Richtungen führten: entweder vollständig .net, C # und das Entity Framework studieren oder ein geeignetes ORM für das vertraute Lazarus-System finden. Es gab auch einen dritten, ersten, unauffälligen Radweg - ORM zu schreiben, um Ihren Bedürfnissen selbst zu entsprechen, aber das ist jetzt nicht der Punkt.

Der Quellcode des Frameworks wird nicht viel kommentiert, aber die Entwickler haben dennoch (anscheinend in der Anfangsphase der Entwicklung) eine bestimmte Menge an Dokumentation vorbereitet. Natürlich ist alles englischsprachig, und die Erfahrung zeigt, dass trotz der Fülle an Code, Diagrammen und Vorlagenprogrammierphrasen viele russischsprachige Programmierer in der englischsprachigen Dokumentation immer noch schlecht orientiert sind. Nicht immer und nicht jeder hat den Wunsch, die Fähigkeit zu trainieren, den englischen technischen Text zu verstehen, ohne dass der Verstand ihn ins Russische übersetzen muss.

Durch wiederholtes Korrekturlesen des zu übersetzenden Textes können Sie außerdem sehen, was ich beim ersten Treffen mit der Dokumentation verpasst habe. Ich habe es nicht vollständig oder falsch verstanden. Das heißt, dies ist für ihn eine Gelegenheit, den untersuchten Rahmen besser zu lernen.

2. In der Dokumentation überspringt der Autor absichtlich oder nicht einige Codeteile, was seiner Meinung nach wahrscheinlich offensichtlich ist. Aufgrund der Einschränkungen beim Schreiben werden in der Dokumentation veraltete Mechanismen und Objekte als Beispiele verwendet, die in neuen Versionen des Frameworks gelöscht oder nicht mehr verwendet werden (habe ich nicht gesagt, dass es sich selbst weiterentwickelt?). Als ich die entwickelten Beispiele selbst wiederholte, fand ich einige Fehler, die behoben werden sollten. Daher erlaubte ich mir an einigen Stellen, den Text nicht nur zu übersetzen, sondern ihn auch zu ergänzen oder zu überarbeiten, damit er relevant bleibt, und die Beispiele funktionierten.

Ich möchte mit der Übersetzung von Materialien aus einem Artikel von Peter Henrikson über den ersten „Wal“ beginnen, auf dem das gesamte Framework steht - die Besuchervorlage. Originaltext hier gepostet .

Besucher- und tiOPF-Vorlage


In diesem Artikel wird die Besuchervorlage vorgestellt, deren Verwendung eines der Hauptkonzepte des tiOPF-Frameworks (TechInsite Object Persistence Framework) ist. Wir werden das Problem im Detail betrachten, nachdem wir alternative Lösungen analysiert haben, bevor wir den Besucher verwenden. Bei der Entwicklung unseres eigenen Besucherkonzepts stehen wir vor einer weiteren Herausforderung: der Notwendigkeit, alle Objekte in der Sammlung zu durchlaufen. Dieses Problem wird ebenfalls untersucht.

Die Hauptaufgabe besteht darin, eine allgemeine Methode zu finden, um eine Reihe verwandter Methoden für einige Objekte in der Sammlung auszuführen. Die durchgeführten Methoden können je nach internem Status der Objekte variieren. Wir können überhaupt keine Methoden ausführen, aber wir können mehrere Methoden für dieselben Objekte ausführen.

Das notwendige Ausbildungsniveau


Der Leser sollte mit dem Objekt Pascal vertraut sein und die Grundprinzipien der objektorientierten Programmierung beherrschen.

Beispiel einer Geschäftsaufgabe in diesem Artikel


Als Beispiel entwickeln wir ein Adressbuch, mit dem Sie Aufzeichnungen über Personen und deren Kontaktinformationen erstellen können. Mit der Zunahme der möglichen Kommunikationswege zwischen Personen sollte die Anwendung es Ihnen ermöglichen, solche Methoden flexibel ohne nennenswerte Codeverarbeitung hinzuzufügen (ich erinnere mich, dass ich den Code zum Hinzufügen einer Telefonnummer sofort erneut verarbeiten musste, um E-Mails hinzuzufügen). Wir müssen zwei Kategorien von Adressen angeben: echte Adressen wie Privatadresse, Post, Arbeit und Elektronik: Festnetztelefon, Fax, Handy, E-Mail, Website.

Auf Präsentationsebene sollte unsere Anwendung wie Explorer / Outlook aussehen, dh sie sollte Standardkomponenten wie TreeView und ListView verwenden. Die Anwendung sollte schnell funktionieren und nicht den Eindruck einer umfangreichen Client-Server-Software erwecken.

Eine Anwendung könnte ungefähr so ​​aussehen:



Im Kontextmenü der Baumstruktur können Sie den Kontakt einer Person oder eines Unternehmens hinzufügen / entfernen und mit der rechten Maustaste auf die Kontaktdatenliste klicken, um deren Bearbeitungsdialog aufzurufen, Daten zu löschen oder hinzuzufügen.

Daten können in verschiedenen Formen gespeichert werden. In Zukunft werden wir überlegen, wie diese Vorlage zur Implementierung dieser Funktion verwendet werden kann.

Bevor Sie anfangen


Wir beginnen mit einer einfachen Sammlung von Objekten - einer Liste von Personen, die wiederum zwei Eigenschaften haben - Name (Name) und Adresse (EmailAdrs). Zunächst wird die Liste mit Daten im Konstruktor gefüllt und anschließend aus einer Datei oder Datenbank geladen. Dies ist natürlich ein sehr vereinfachtes Beispiel, aber es reicht aus, um die Besuchervorlage vollständig zu implementieren.

Erstellen Sie eine neue Anwendung und fügen Sie zwei Klassen des Schnittstellenabschnitts des Hauptmoduls hinzu: TPersonList (von TObjectList geerbt und erfordert die Verbindung des Contnrs-Moduls in Verwendungen) und TPerson (von TObject geerbt):

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; 

Im TPersonList-Konstruktor erstellen wir drei TPerson-Objekte und fügen sie der Liste hinzu:

 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; 

Zuerst gehen wir die Liste durch und führen zwei Operationen für jedes Element der Liste aus. Die Vorgänge sind ähnlich, aber nicht identisch: Ein einfacher ShowMessage-Aufruf, der den Inhalt der Eigenschaften Name und EmailAdrs von TPerson-Objekten anzeigt. Fügen Sie dem Formular zwei Schaltflächen hinzu und benennen Sie sie wie folgt:



Im bevorzugten Bereich Ihres Formulars sollten Sie außerdem eine Eigenschaft (oder nur ein Feld) FPersonList vom Typ TPersonList hinzufügen (wenn der Typ unter dem Formular deklariert ist, ändern Sie entweder die Reihenfolge oder erstellen Sie eine vorläufige Typdeklaration) und rufen Sie den Konstruktor im onCreate-Ereignishandler auf:

 FPersonList := TPersonList.Create; 

Um den Speicher im onClose-Ereignishandler des Formulars ordnungsgemäß freizugeben, muss dieses Objekt zerstört werden:

 FPersonList.Free. 

Schritt 1. Hardcode-Iteration


Fügen Sie dem onClick-Ereignishandler der ersten Schaltfläche den folgenden Code hinzu, um Namen von TPerson-Objekten anzuzeigen:

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

Für die zweite Schaltfläche lautet der Handlercode wie folgt:

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

Hier sind die offensichtlichen Schulen dieses Codes:

  • zwei Methoden, die fast dasselbe tun. Der Unterschied besteht nur im Namen der Eigenschaft des Objekts, das sie anzeigen.
  • Die Iteration ist langwierig, insbesondere wenn Sie gezwungen sind, eine ähnliche Schleife an hundert Stellen im Code zu schreiben.
  • Eine harte Besetzung für TPerson ist mit Ausnahmesituationen behaftet. Was ist, wenn die Liste eine Instanz von TAnimal ohne Adresseneigenschaft enthält? In diesem Code gibt es keinen Mechanismus, um den Fehler zu stoppen und sich dagegen zu verteidigen.

Lassen Sie uns herausfinden, wie der Code durch Einführung einer Abstraktion verbessert werden kann: Wir übergeben den Iteratorcode an die übergeordnete Klasse.

Schritt 2. Iterator abstrahieren


Wir wollen also die Iteratorlogik in die Basisklasse verschieben. Der Listeniterator selbst ist sehr einfach:

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

Es hört sich so an, als würden wir eine Iterator- Vorlage verwenden. Aus dem Buch über das Gang-of-Four-Entwurfsmusterbuch ist bekannt, dass der Iterator extern und intern sein kann. Bei Verwendung eines externen Iterators steuert der Client die Durchquerung explizit durch Aufrufen der Next-Methode (z. B. wird die Aufzählung von TCollection-Elementen durch die Methoden First, Next, Last gesteuert). Wir werden hier den internen Iterator verwenden, da es einfacher ist, die Baumdurchquerung mit seiner Hilfe zu implementieren, was unser Ziel ist. Wir werden die Iterate-Methode zu unserer Listenklasse hinzufügen und ihr eine Rückrufmethode übergeben, die für jedes Element der Liste ausgeführt werden muss. Rückruf in Objekt Pascal wird als prozeduraler Typ deklariert, wir haben zum Beispiel TDoSomethingToAPerson.

Daher deklarieren wir einen prozeduralen Typ TDoSomethingToAPerson, der einen Parameter vom Typ TPerson verwendet. Mit dem Prozedurtyp können Sie die Methode als Parameter einer anderen Methode verwenden, dh einen Rückruf implementieren. Auf diese Weise erstellen wir zwei Methoden, von denen eine die Name-Eigenschaft des Objekts und die andere die EmailAdrs-Eigenschaft anzeigt und die selbst als Parameter an den allgemeinen Iterator übergeben werden. Schließlich sollte der Abschnitt zur Typdeklaration folgendermaßen aussehen:

 { 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; 

Um nun die erforderlichen Aktionen für die Listenelemente auszuführen, müssen wir zwei Dinge tun. Definieren Sie zunächst die erforderlichen Operationen mit Methoden, deren Signatur von TDoSomethingToAPerson angegeben wurde, und schreiben Sie zweitens DoSomething-Aufrufe mit den Zeigern auf diese als Parameter übergebenen Methoden. Fügen Sie im Abschnitt Formularbeschreibung zwei Deklarationen hinzu:

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

Bei der Implementierung dieser Methoden geben wir Folgendes an:

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

Der Code für Schaltflächenhandler wird wie folgt geändert:

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

Schon besser. Wir haben jetzt drei Abstraktionsebenen in unserem Code. Ein generischer Iterator ist eine Klassenmethode, die eine Sammlung von Objekten implementiert. Geschäftslogik (bisher nur endlose Nachrichtenausgabe über ShowMessage) wird separat platziert. Auf der Ebene der Präsentation (grafische Oberfläche) wird die Geschäftslogik in einer Zeile aufgerufen.

Es ist leicht vorstellbar, wie ein Aufruf von ShowMessage durch Code ersetzt werden kann, der unsere Daten von TPerson in einer relationalen Datenbank mithilfe der SQL-Abfrage des TQuery-Objekts speichert. Zum Beispiel so:

 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; 

Dies führt übrigens zu einem neuen Problem bei der Aufrechterhaltung einer Verbindung zur Datenbank. In unserer Anfrage wird die Verbindung zur Datenbank über ein globales gAppDatabase-Objekt hergestellt. Aber wo wird es sich befinden und wie soll man arbeiten? Darüber hinaus werden wir bei jedem Schritt des Iterators gequält, um TQuery-Objekte zu erstellen, die Verbindung zu konfigurieren, die Abfrage auszuführen und nicht zu vergessen, den Speicher freizugeben. Es ist besser, diesen Code in eine Klasse zu packen, die die Logik zum Erstellen und Ausführen von SQL-Abfragen sowie zum Einrichten und Aufrechterhalten einer Verbindung zur Datenbank enthält.

Schritt 3. Übergeben eines Objekts anstelle eines Zeigers auf einen Rückruf


Durch Übergeben des Objekts an die Iteratormethode der Basisklasse wird das Problem der Statusverwaltung gelöst. Wir werden eine abstrakte Besucherklasse TPersonVisitor mit einer einzelnen Execute-Methode erstellen und das Objekt als Parameter an diese Methode übergeben. Die abstrakte Besucheroberfläche ist unten dargestellt:

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

Fügen Sie als Nächstes die Iterate-Methode zu unserer TPersonList-Klasse hinzu:

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

Die Implementierung dieser Methode erfolgt wie folgt:

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

Ein Objekt des implementierten Visitors der TPersonVisitor-Klasse wird an die Iterate-Methode übergeben, und beim Durchlaufen der Listenelemente für jedes dieser Elemente wird der angegebene Visitor (seine Ausführungsmethode) mit der TPerson-Instanz als Parameter aufgerufen.

Erstellen wir zwei Implementierungen des Visitors - TShowNameVisitor und TShowEmailVistor, die die erforderlichen Arbeiten ausführen. So füllen Sie den Abschnitt mit den Modulschnittstellen auf:

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

Der Einfachheit halber besteht die Implementierung der Ausführungsmethoden für sie weiterhin aus einer einzelnen Zeile - ShowMessage (pPerson.Name) und ShowMessage (pPerson.EMailAdrs).

Ändern Sie den Code für die Schaltflächenklick-Handler:

 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; 

Nachdem wir ein Problem gelöst haben, haben wir uns ein anderes geschaffen. Die Iteratorlogik ist in einer separaten Klasse gekapselt. Die während der Iteration ausgeführten Operationen sind in Objekte eingeschlossen, wodurch wir einige Informationen über den Status speichern können. Die Größe des Codes ist jedoch von einer Zeile (FPersonList.DoSomething (@DoShowName) auf neun Zeilen für jeden Schaltflächenhandler angewachsen. Jetzt wird es uns helfen - dies ist der Besuchermanager, der sich um das Erstellen und Freigeben seiner Kopien kümmert. Möglicherweise können wir mehrere Vorgänge mit Objekten vorsehen, die während der Iteration ausgeführt werden sollen. Dazu speichert der Besuchermanager seine Liste und geht sie bei jedem Schritt durch, Sie CREATE, DELETE und UPDATE:. Durch drei verschiedene Operatoren SQL werden kann, speichern Olnyaya nur Weiter ausgewählte Operationen werden die Vorteile dieses Ansatzes deutlich zeigen, werden wir die Besucher, um die Daten in einer relationalen Datenbank als eine einfache Datensicherung Betriebes durchgeführt.

Schritt 4. Weitere Verkapselung des Besuchers


Bevor wir fortfahren, müssen wir die Logik der Arbeit des Besuchers kapseln und sie von der Geschäftslogik der Anwendung trennen, damit sie nicht zu ihr zurückkehrt. Dazu benötigen wir drei Schritte: Erstellen Sie die Basisklassen TVisited und TVisitor, dann die Basisklassen für das Geschäftsobjekt und die Sammlung von Geschäftsobjekten, und passen Sie dann unsere spezifischen Klassen TPerson und TPersonList (oder TPeople) geringfügig an, sodass sie Erben der erstellten Basis werden Klassen. Im Allgemeinen entspricht die Klassenstruktur einem solchen Diagramm:



Das TVisitor-Objekt implementiert zwei Methoden: die AcceptVisitor-Funktion und die Execute-Prozedur, an die das TVisited-Typobjekt übergeben wird. Das TVisited-Objekt implementiert wiederum die Iterate-Methode mit einem Parameter vom Typ TVisitor. Das heißt, TVisited.Iterate muss die Execute-Methode für das übertragene TVisitor-Objekt aufrufen und einen Link zu seiner eigenen Instanz als Parameter senden. Wenn die Instanz eine Sammlung ist, wird die Execute-Methode für jedes Element in der Sammlung aufgerufen. Die AcceptVisitor-Funktion ist erforderlich, da wir ein verallgemeinertes System entwickeln. Es ist möglich, eine Instanz der TDog-Klasse an den Besucher zu übergeben, der nur mit TPerson-Typen arbeitet, und es muss einen Mechanismus geben, um Ausnahmen und Zugriffsfehler aufgrund von Typinkongruenz zu verhindern. Die TVisited-Klasse ist der Nachkomme der TPersistent-Klasse, da wir wenig später Funktionen implementieren müssen, die sich auf die Verwendung von RTTI beziehen.

Der Schnittstellenteil des Moduls sieht nun folgendermaßen aus:

 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; 

Die Methoden der TVisitor Abstract-Klasse werden von den Erben implementiert, und die allgemeine Implementierung der Iterate-Methode für TVisited ist unten angegeben:

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

Gleichzeitig wird die Methode für die Möglichkeit ihrer Überschreibung bei den Erben als virtuell deklariert.

Schritt 5. Erstellen Sie ein freigegebenes Geschäftsobjekt und eine Sammlung


Unser Framework benötigt zwei weitere Basisklassen: das Definieren eines Geschäftsobjekts und eine Sammlung solcher Objekte. Nennen Sie sie TtiObject und TtiObjectList. Die Schnittstelle des ersten von ihnen:

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

Später im Entwicklungsprozess werden wir diese Klasse komplizieren, aber für die aktuelle Aufgabe reicht nur ein virtueller Konstruktor mit der Möglichkeit, sie in den Erben zu überschreiben.

Wir planen, die TtiObjectList-Klasse aus TVisited zu generieren, um das Verhalten in Methoden zu verwenden, die bereits vom Vorfahren implementiert wurden (es gibt auch andere Gründe für diese Vererbung, die an seiner Stelle erörtert werden). Darüber hinaus verbietet nichts die Verwendung von Schnittstellen (Interfaces) anstelle von abstrakten Klassen.

Der Schnittstellenteil der TtiObjectList-Klasse lautet wie folgt:

 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; 

Wie Sie sehen, befindet sich der Container selbst mit den Objektelementen im geschützten Bereich und steht Kunden dieser Klasse nicht zur Verfügung. Der wichtigste Teil der Klasse ist die Implementierung der überschriebenen Iterate-Methode. Wenn in der Basisklasse die Methode einfach pVisitor.Execute (self) heißt, dann ist hier die Implementierung mit der Aufzählung der Liste verbunden:

 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; 

Die Implementierung anderer Klassenmethoden erfordert eine Codezeile, ohne automatisch platzierte geerbte Ausdrücke zu berücksichtigen:

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

Dies ist ein wichtiger Teil des gesamten Systems. Wir haben zwei grundlegende Klassen von Geschäftslogik: TtiObject und TtiObjectList. Beide haben eine Iterate-Methode, an die eine Instanz der TVisited-Klasse übergeben wird. Der Iterator selbst ruft die Execute-Methode der TVisitor-Klasse auf und übergibt ihr einen Verweis auf das Objekt selbst. Dieser Aufruf ist im Klassenverhalten auf der obersten Vererbungsebene vordefiniert. Für eine Containerklasse verfügt jedes in der Liste gespeicherte Objekt auch über eine Iterate-Methode, die mit einem Parameter vom Typ TVisitor aufgerufen wird. Das heißt, es wird garantiert, dass jeder bestimmte Besucher alle in der Liste gespeicherten Objekte sowie die Liste selbst als Containerobjekt umgeht.

Schritt 6. Erstellen eines Besuchermanagers


Zurück zu dem Problem, das wir selbst beim dritten Schritt gezogen haben. Da wir nicht jedes Mal Kopien von Besuchern erstellen und zerstören möchten, ist die Entwicklung des Managers die Lösung. Es sollte zwei Hauptaufgaben ausführen: Verwalten der Liste der Besucher (die im Initialisierungsabschnitt der einzelnen Module als solche registriert sind) und Ausführen, wenn sie den entsprechenden Befehl vom Client erhalten.
Zur Implementierung des Managers ergänzen wir unser Modul um drei zusätzliche Klassen: TVisClassRef, TVisMapping und TtiVisitorManager.

 TVisClassRef = class of TVisitor; 

TVisClassRef ist ein Referenztyp und gibt den Namen einer bestimmten Klasse an - ein Nachkomme von TVisitor. Die Verwendung eines Referenztyps hat folgende Bedeutung: Wenn die Base Execute-Methode mit einer Signatur aufgerufen wird

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

Intern kann diese Methode einen Ausdruck wie lVisitor: = pVisClass.Create verwenden, um eine Instanz eines bestimmten Besuchers zu erstellen, ohne zuvor den Typ zu kennen. Das heißt, jede Klasse - ein Nachkomme von TVisitor kann dynamisch innerhalb derselben Execute-Methode erstellt werden, wenn der Name seiner Klasse als Parameter übergeben wird.

Die zweite Klasse, TVisMapping, ist eine einfache Datenstruktur mit zwei Eigenschaften: einem Verweis auf den Typ TVisClassRef und einem String-Eigenschaft Command. Eine Klasse wird benötigt, um die Operationen zu vergleichen, die mit ihrem Namen (einem Befehl, z. B. "Speichern") ausgeführt werden, und der Besucherklasse, die diese Befehle ausführen. Fügen Sie dem Projekt seinen Code hinzu:

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

Und die letzte Klasse ist TtiVisitorManager. Wenn wir den Besucher mit dem Manager registrieren, wird eine Instanz der TVisMapping-Klasse erstellt, die in die Managerliste eingetragen wird.
Daher wird im Manager eine Liste von Besuchern mit einem übereinstimmenden Zeichenfolgenbefehl erstellt, nach dessen Empfang sie ausgeführt werden. Die Klassenschnittstelle wird dem Modul hinzugefügt:

 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; 

Die wichtigsten Methoden sind RegisterVisitor und Execute. Der erste wird normalerweise im Initialisierungsabschnitt des Moduls aufgerufen, der die Visitor-Klasse beschreibt und ungefähr so ​​aussieht:

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

Der Code der Methode selbst lautet wie folgt:

 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; 

Es ist nicht schwer zu bemerken, dass dieser Code der Pascal-Implementierung der Factory- Vorlage sehr ähnlich ist.

Eine andere wichtige Execute-Methode akzeptiert zwei Parameter: den Befehl, mit dem der Besucher oder seine zu identifizierende Gruppe identifiziert wird, sowie das Datenobjekt, dessen Iterate-Methode mit einem Link zur Instanz des gewünschten Besuchers aufgerufen wird. Der vollständige Code für die Execute-Methode ist unten angegeben:

 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; 

Um zwei zuvor registrierte Besucher mit einem Team zu betreiben, benötigen wir nur eine Codezeile:

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

Als nächstes werden wir unser Projekt ergänzen, damit Sie ähnliche Befehle aufrufen können:

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

Schritt 7. Anpassen von Geschäftslogikklassen


Durch Hinzufügen des Vorfahren der Klassen TtiObject und TtiObjectList für unsere Geschäftsobjekte TPerson und TPeople können wir die Iteratorlogik in der Basisklasse kapseln und nicht mehr berühren. Außerdem können Objekte mit Daten an den Visitor Manager übertragen werden.

Die neue Containerklassendeklaration sieht folgendermaßen aus:

 TPeople = class(TtiObjectList); 

Tatsächlich muss die TPeople-Klasse nicht einmal etwas implementieren. Theoretisch könnten wir überhaupt auf eine TPeople-Deklaration verzichten und Objekte in einer Instanz der TtiObjectList-Klasse speichern. Da wir jedoch planen, Besucher zu schreiben, die nur TPeople-Instanzen verarbeiten, benötigen wir diese Klasse. In der AcceptVisitor-Funktion werden folgende Überprüfungen durchgeführt:

 Result := pVisited is TPeople. 

Für die TPerson-Klasse fügen wir den TtiObject-Vorfahren hinzu und verschieben die beiden vorhandenen Eigenschaften in den veröffentlichten Bereich, da wir in Zukunft RTTI mit diesen Eigenschaften bearbeiten müssen. Dies wird viel später den Code für die Zuordnung von Objekten und Datensätzen in einer relationalen Datenbank erheblich reduzieren:

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

Schritt 8. Erstellen Sie eine Prototypansicht


Bemerkung . Im ursprünglichen Artikel basierte die GUI auf den Komponenten, die der Autor von tiOPF erstellt hatte, um die Arbeit mit seinem Framework in Delphi zu vereinfachen. Dies waren Analoga von DB Aware-Komponenten, bei denen es sich um Standardsteuerelemente wie Beschriftungen, Eingabefelder, Kontrollkästchen, Listen usw. handelte, die jedoch bestimmten Eigenschaften von tiObject-Objekten auf dieselbe Weise zugeordnet wurden wie Datenanzeigekomponenten Feldern in Datenbanktabellen. Im Laufe der Zeit hat der Autor des Frameworks Pakete mit diesen visuellen Komponenten als veraltet und unerwünscht markiert. Im Gegenzug schlägt er vor, mithilfe des Mediator-Entwurfsmusters eine Verknüpfung zwischen visuellen Komponenten und Klasseneigenschaften herzustellen.Diese Vorlage ist die zweitwichtigste in der gesamten Architektur des Frameworks. Die Beschreibung des Vermittlers durch den Autor wird in einem separaten Artikel behandelt, dessen Umfang mit diesem Handbuch vergleichbar ist. Daher biete ich meine vereinfachte Version hier als GUI an.

Benennen Sie die Schaltfläche 1 im Projektformular in "Befehl anzeigen" um, und Schaltfläche 2 lässt sie entweder vorerst ohne Handler oder nennt sie sofort "Befehl speichern". Werfen Sie eine Memokomponente auf das Formular und platzieren Sie alle Elemente nach Ihrem Geschmack.

Fügen Sie eine Besucherklasse hinzu, die den Befehl show implementiert:

Interface -

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

Und die Implementierung ist -
 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 überprüft, ob das übertragene Objekt eine Instanz von TPerson ist, da der Besucher den Befehl nur mit solchen Objekten ausführen sollte. Wenn der Typ übereinstimmt, wird der Befehl ausgeführt und dem Textfeld eine Zeile mit Objekteigenschaften hinzugefügt.

Unterstützende Maßnahmen für den Zustand des Codes sind wie folgt. Fügen Sie der Beschreibung des Formulars selbst im privaten Bereich zwei Eigenschaften hinzu: FPeople vom Typ TPeople und VM vom Typ TtiVisitorManager. Im Ereignishandler für die Formularerstellung müssen wir diese Eigenschaften initiieren und den Besucher mit dem Befehl "show" registrieren:

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

FilPeople ist auch eine Hilfsprozedur, die eine Liste mit drei Objekten füllt. Der Code stammt aus dem vorherigen Listenkonstruktor. Vergessen Sie nicht, alle erstellten Objekte zu zerstören. In diesem Fall schreiben wir FPeople.Free und VM.Free in den Handler zum Schließen von Formularen.

Und jetzt - Bams! - Handler der ersten Schaltfläche:

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

Stimmen Sie zu, so viel mehr Spaß. Und schwöre nicht auf den Hash aller Klassen in einem Modul. Ganz am Ende des Handbuchs werden wir diese Trümmer harken.

Schritt 9. Die Basisklasse des Besuchers, der mit Textdateien arbeitet


In dieser Phase erstellen wir die Basisklasse des Besuchers, der weiß, wie man mit Textdateien arbeitet. Es gibt drei Möglichkeiten, mit Dateien im Objektpascal zu arbeiten: alte Prozeduren aus der Zeit des ersten Pascal (wie AssignFile und ReadLn), Streams (TStringStream oder TFileStream) und das TStringList-Objekt.

Wenn die erste Methode sehr veraltet ist, sind die zweite und dritte eine gute Alternative, die auf OOP basiert. Gleichzeitig bietet das Arbeiten mit Streams zusätzliche Vorteile wie die Möglichkeit, Daten zu komprimieren und zu verschlüsseln. Das zeilenweise Lesen und Schreiben in einen Stream ist in unserem Beispiel jedoch eine Art Redundanz. Der Einfachheit halber wählen wir eine TStringList mit zwei einfachen Methoden - LoadFromFile und SaveToFile. Denken Sie jedoch daran, dass diese Methoden bei großen Dateien erheblich langsamer werden, sodass der Stream die optimale Wahl für sie ist.

TVisFile-Basisklassenschnittstelle:

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

Und die Konstruktor- und Destruktor-Implementierung:

 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; 

Der Wert der FFileName-Eigenschaft wird in den Konstruktoren der Nachkommen dieser Basisklasse zugewiesen (verwenden Sie einfach nicht die hier angeordnete Hardcodierung als Hauptprogrammierstil danach!). Das Diagramm der Besucherklassen, die mit Dateien arbeiten



, sieht wie folgt aus: In Übereinstimmung mit dem folgenden Diagramm erstellen wir zwei Nachkommen der TVisFile-Basisklasse: TVisTXTFile und TVisCSVFile. Eine arbeitet mit * .csv-Dateien, in denen Datenfelder durch ein Symbol (Komma) getrennt sind, die zweite mit Textdateien, in denen einzelne Datenfelder eine feste Länge pro Zeile haben. Für diese Klassen definieren wir die Konstruktoren nur wie folgt neu:

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

Schritt 10. Fügen Sie den Visitor-Handler für Textdateien hinzu


Hier werden wir zwei spezifische Besucher hinzufügen, einer liest eine Textdatei, der zweite schreibt darauf. Der lesende Besucher muss die Methoden AcceptVisitor und Execute base class überschreiben. AcceptVisitor überprüft, ob das TPeople-Klassenobjekt an den Besucher übergeben wird:

 Result := pVisited is TPeople; 

Die Ausführungsimplementierung ist wie folgt:

 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; 

Der Besucher löscht zuerst die Liste des TPeople-Objekts, das ihm vom Parameter übergeben wurde, liest dann die Zeilen aus seinem TStringList-Objekt, in das der Inhalt der Datei geladen wird, erstellt in jeder Zeile ein TPerson-Objekt und fügt es der TPeople-Containerliste hinzu. Der Einfachheit halber sind die Eigenschaften name und emailadrs in der Textdatei durch Leerzeichen getrennt.

Der Datensatzbesucher implementiert die inverse Operation. Sein Konstruktor (überschrieben) löscht die interne TStringList (d. H. Führt die FList.Clear-Operation aus; sie ist nach der Vererbung obligatorisch). AcceptVisitor überprüft, ob das TPerson-Klassenobjekt übergeben wurde. Dies ist kein Fehler, sondern ein wichtiger Unterschied zu derselben Besuchermethode. Es scheint einfacher zu sein, die Aufzeichnung auf dieselbe Weise zu implementieren: Scannen Sie alle Containerobjekte, fügen Sie sie einer StringList hinzu und speichern Sie sie dann in einer Datei. All dies war so, wenn wir wirklich über das endgültige Schreiben von Daten in eine Datei sprachen, wir jedoch planen, Daten einer relationalen Datenbank zuzuordnen, sollte dies beachtet werden. In diesem Fall sollten wir den SQL-Code nur für die Objekte ausführen, die geändert (erstellt, gelöscht oder bearbeitet) wurden. Aus diesem Grund, bevor der Besucher eine Operation an dem Objekt ausführt,er muss die Korrespondenz seines Typs überprüfen:

 Result := pVisited is Tperson; 

Die Methode execute fügt der internen StringList einfach eine Zeichenfolge hinzu, die mit der angegebenen Regel formatiert ist: Zuerst der Inhalt der Eigenschaft name des übergebenen Objekts, aufgefüllt mit Leerzeichen mit bis zu 20 Zeichen, dann der Inhalt der Eigenschaft 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; 

Schritt 11. Fügen Sie den Visitor-Handler für CSV-Dateien hinzu


Besucher des Lesens und Schreibens sind in fast allen Kollegen aus TXT-Klassen ähnlich, mit Ausnahme der Art und Weise, wie die letzte Zeile einer Datei formatiert wird: Im CSV-Standard werden Eigenschaftswerte durch Kommas getrennt. Um Zeilen zu lesen und in Eigenschaften zu analysieren, verwenden wir die ExtractDelimited-Funktion aus dem Strutils-Modul. Das Schreiben erfolgt durch einfaches Verketten der Zeilen:

 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; 

Wir müssen nur noch neue Besucher im Manager registrieren und den Betrieb der Anwendung überprüfen. Fügen Sie im Handler für die Formularerstellung den folgenden Code hinzu:

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

Docken Sie die erforderlichen Schaltflächen im Formular an und weisen Sie ihnen die entsprechenden Handler zu:



 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; 

Zusätzliche Dateiformate zum Speichern von Daten werden implementiert, indem einfach die entsprechenden Besucher hinzugefügt und im Manager registriert werden. Beachten Sie Folgendes: Wir haben die Befehle absichtlich anders benannt, dh saveTXT und saveCSV. Wenn beide Besucher mit einem Speicherbefehl übereinstimmen, starten beide mit demselben Befehl. Überprüfen Sie dies selbst.

Schritt 12. Endgültige Codebereinigung


Um die Schönheit und Reinheit des Codes zu erhöhen und ein Projekt für die Weiterentwicklung der Interaktion mit dem DBMS vorzubereiten, werden wir unsere Klassen entsprechend der Logik und ihrem Zweck in verschiedene Module verteilen. Letztendlich sollten wir die folgende Struktur von Modulen im Projektordner haben, die es uns ermöglicht, auf eine kreisförmige Beziehung zwischen ihnen zu verzichten (ordnen Sie beim Zusammenstellen die erforderlichen Module in Verwendungsabschnitten an):

Modul
Funktion
Klassen
tivisitor.pas
Basisklassen der Besucher- und Manager-Vorlage
TVisitor
TVisited
TVisMapping
TtiVisitorManager
tiobject.pas
Basisgeschäftslogikklassen
TtiObject
TtiObjectList
people_BOM.pas
Spezifische Geschäftslogikklassen
TPerson
TPeople
people_SRV.pas
Konkrete Klassen, die für die Interaktion verantwortlich sind
TVisFile
TVisTXTFile
TVisCSVFile
TVisCSVSave
TVisCSVRead
TVisTXTSave
TVisTXTRead

Fazit


In diesem Artikel haben wir das Problem der Iteration über eine Sammlung oder Liste von Objekten untersucht, die unterschiedliche Typen haben können. Wir haben die von GoF vorgeschlagene Besuchervorlage verwendet, um zwei verschiedene Methoden zum Zuordnen von Daten von Objekten zu Dateien unterschiedlicher Formate optimal zu implementieren. Gleichzeitig können durch die Erstellung des Besuchermanagers verschiedene Methoden von einem Team ausgeführt werden. Letztendlich helfen uns die einfachen und anschaulichen Beispiele, die im Artikel diskutiert werden, ein ähnliches System für die Zuordnung von Objekten zu einer relationalen Datenbank weiterzuentwickeln.

Archiv mit Quellcode von Beispielen - hier

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


All Articles