From translator
There are two reasons why I undertook to translate several materials on the framework developed not twenty years ago for the most popular programming environment today:
1. A few years ago, having learned many of the charms of working with the Entity Framework as ORM for the .Net platform, I vainly looked for counterparts for the Lazarus environment and, in general, for freepascal.
Surprisingly, there are no good ORMs for it. All that then managed to find - an open-source project called
tiOPF , developed in the late 90s for delphi, later ported to freepascal. However, this framework is fundamentally different from the usual form of large and thick ORM.
There are no visual ways of designing objects (in Entity - model first) and matching objects with fields of relational database tables (in Entity - database first) in tiOPF. The developer himself positions this fact as one of the drawbacks of the project, but as a virtue it offers a full orientation specifically to the object business model, it’s worth only once ...
It was at the level of the proposed hardcoding that I had problems. At that time, I was not very well versed in the paradigms and methods that the framework developer used in full and mentioned in the documentation several times per paragraph (visitor, linker, observer design patterns, several levels of abstraction for DBMS independence, etc. .). My big project working with a database at that time was completely focused on the visual components of Lazarus and the way of working with databases offered by the visual environment, as a result - tons of the same code: three tables in the database itself with almost the same structure and homogeneous data three identical forms for viewing, three identical forms for editing, three identical forms for reports and all the rest from the top heading “how not to design software”.
')
After reading a lot of literature on the principles of proper database design and information systems, including the study of templates, and also getting acquainted with the Entity Framework, I decided to do a full refactoring of both the database itself and my application. And if I coped with the first task, then for the implementation of the second there were two roads going in different directions: either completely go to the study of .net, C # and the Entity Framework, or find a suitable ORM for the usual Lazarus system. There was also a third inconspicuous cycling trail - to write the ORM for your own needs, but this is not the point now.
The source code of the framework is little commented, however, the developers still prepared (apparently in the initial development period) a certain amount of documentation. All of it, of course, is English-speaking, and experience shows that, despite the abundance of code, diagrams and patterned programmer phrases, many Russian-speaking programmers are still poorly oriented in English-language documentation. Not always and not everyone has the desire to train the ability to understand English technical text without the need for the mind to translate it into Russian.
In addition, re-reading the text for translation allows you to see what I missed when I first got acquainted with the documentation, did not understand completely or incorrectly. That is, it is also an opportunity for oneself to better assimilate the studied framework.
2. In the documentation, the author intentionally or not skips some pieces of code, probably obvious in his opinion. In connection with the prescription of its writing, the documentation uses as examples obsolete mechanisms and objects deleted or no longer used in new versions of the framework (did I not say that it continues to evolve?). Also, when I repeated the examples I developed myself, I found some errors that should be corrected. Therefore, in some places I allowed myself not only to translate the text, but also to supplement or rework it so that it remains relevant, and the examples are working.
I want to begin the translation of materials from the article by Peter Henrikson about the first “whale” on which the whole framework - the Visitor template stands.
The original text is posted here .
Template Visitor and tiOPF
The purpose of this article is to introduce the Visitor template, the use of which is one of the main concepts of the tiOPF framework (TechInsite Object Persistence Framework). We will look at the problem in detail, after analyzing alternative solutions before using the Visitor. In the process of developing your own concept of the Visitor, we will face another task: the need to iterate all the objects in the collection. This issue will also be studied.
The main task is to come up with a generalized way of executing a set of related methods on some objects of the collection. The methods performed may vary depending on the internal state of the objects. We can not execute methods at all, but we can execute several methods on the same objects.
Required level of training
The reader must be familiar with object pascal and be familiar with the basic principles of object-oriented programming.
Sample business problem in this article
As an example, we will develop an address book that allows you to create records about people and their contact information. With the increase in possible ways of communication between people, the application should be able to flexibly add such methods without significantly reworking the code (I remember once, once you had finished reworking the code to add a phone number, you immediately needed to rework it to add e-mail). We need to provide two categories of addresses: real, such as home address, postal, work, and electronic: landline telephone, fax, mobile, e-mail, website.
At the presentation level, our application should look like Explorer / Outlook, that is, it is supposed to use standard components such as TreeView and ListView. The application should work quickly and not give the impression of bulky client-server software.
The application might look like this:

In the context menu of the tree, you can choose to add / delete a contact of a person or company, and right-click on the list of contact data - to call the dialog to edit them, delete or add data.
The data can be saved in various forms, and in the future we will look at how to use this Adapter template to implement this feature.
Before the start
We will begin work with a simple collection of objects — a list of people who, in turn, have two properties — a name (Name) and an address (EmailAdrs). To begin with, the list will be populated with data in the constructor, and later - loaded from a file or database. Of course, this is a very simplified example, but it is sufficient for the full implementation of the Visitor pattern.
Create a new application and add two classes of the interface module section of the main module: TPersonList (inherits from TObjectList and requires connection in the uses of the contnrs module) and TPerson (inherited from 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;
In the TPersonList constructor, we will create three TPerson objects and add them to the list:
constructor TPersonList.Create; var lData: TPerson; begin inherited; lData := TPerson.Create; lData.Name := 'Malcolm Groves'; lData.EMailAdrs := 'malcolm@dontspamme.com';
To begin, we will go through the list and perform two operations on each item in the list. The operations are similar, but not identical: a simple call to ShowMessage with the output of the contents of the Name and EmailAdrs properties of TPerson objects. Add two buttons to the form and name them like this:

In the preferred field of view of your form, you should also add a property (or just a field) FersonList of type TPersonList (if the type is declared below the form, then either change the order or make a preliminary type declaration), and call the constructor in the onCreate event handler:
FPersonList := TPersonList.Create;
To properly free memory in the form's onClose event handler, this object must be destroyed:
FPersonList.Free.
Step 1. Hardcoding iteration
To display names from TPerson objects, add the following code to the first button's onClick event handler:
procedure TForm1.Button1Click(Sender: TObject); var i: integer; begin for i := 0 to FPersonList.Count - 1 do ShowMessage(TPerson(FPersonList.Items[i]).Name); end;
For the second button, the handler code will be as follows:
procedure TForm1.Button2Click(Sender: TObject); var i: integer; begin for i := 0 to FPersonList.Count - 1 do ShowMessage(TPerson(FPersonList.Items[i]).EMailAdrs); end;
Here are the obvious jambs of this code:
- two methods that do almost the same thing. The only difference is in the name of the property of the object, which they show;
- the iteration will be tedious, especially when you have to write a similar loop in a hundred places in the code;
- hard type casting to TPerson is fraught with exceptional situations. What if the list contains a copy of TAnimal without an address property? There is no mechanism to stop the error and protect against it in this code.
Let's figure out how to improve the code by introducing an abstraction: pass the iterator code to the parent class.
Step 2. Abstraction iterator
So, we want to bring the logic of the iterator to the base class. The list iterator itself is very simple:
for i := 0 to FList.Count - 1 do
It sounds as if we are planning to use the
Iterator pattern. From the book about the design patterns of the gang of four (
Gang-of-Four design patterns book ) it is known that the Iterator is external and internal. When using an external iterator, the bypass process is explicitly controlled by the client by calling the Next method (for example, the iteration of the TCollection elements is controlled by the First, Next, Last methods). We will use an internal iterator here, since with its help it is easier to implement tree traversal, which is our goal. To our list class, we will add an Iterate method and will pass to it a callback method that must be performed on each element of the list. Callback in object pascal is declared as a procedural type, we will have, for example, TDoSomethingToAPerson.
So, we declare the procedural type TDoSomethingToAPerson, which takes one parameter of type TPerson. A procedural type allows you to use a method as a parameter of another method, that is, to implement a callback. In this way, we will create two methods, one of which will show the Name property of the object, and the other the EmailAdrs property, and they will be passed as a parameter for the general iterator. The final type declaration section should look like this:
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 = 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;
Now, to perform the necessary actions on the elements of the list, we need to do two things. First, determine the necessary operations using methods that have the signature specified by TDoSomethingToAPerson, and second, write calls to DoSomething with passing pointers to these methods as a parameter. In the form description section, add two declarations:
private FPersonList: TPersonList; procedure DoShowName(const pData: TPerson); procedure DoShowEmail(const pData: TPerson);
In the implementation of these methods, we indicate:
procedure TForm1.DoShowName(const pData: TPerson); begin ShowMessage(pData.Name); end; procedure TForm1.DoShowEmail(const pData: TPerson); begin ShowMessage(pData.EMailAdrs); end;
The code for button handlers is changed as follows:
procedure TForm1.Button1Click(Sender: TObject); begin FPersonList.DoSomething(@DoShowName); end; procedure TForm1.Button2Click(Sender: TObject); begin FPersonList.DoSomething(@DoShowEmail); end;
Already better. Now we have three levels of abstraction in the code. A common iterator is a class method that implements a collection of objects. Business logic (for the time being just infinite output of messages via ShowMessage) is located separately. At the presentation level (GUI), the business logic is invoked in a single line.
It is easy to imagine how the ShowMessage call can be replaced with code that saves our data from TPerson in a relational database using a SQL query of the TQuery object. For example, like this:
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;
By the way, this introduces a new problem of maintaining a database connection. In our request, the connection to the database is carried out through a certain global object gAppDatabase. But where will it be located and how to work? In addition, at every iterator step, we will torture ourselves to create TQuery objects, set up a connection, execute a query, and not forget to free up memory. It would be better to wrap this code in a class that encapsulates the logic of creating and executing SQL queries, as well as setting up and maintaining a connection to the database.
Step 3. Passing an object instead of passing a pointer to callback
Passing an object to the base class iterator method will solve the state support problem. We will create an abstract Visitors class TPersonVisitor with a single Execute method and pass the object to this method as a parameter. The abstract Visitor interface is shown below:
TPersonVisitor = class(TObject) public procedure Execute(pPerson: TPerson); virtual; abstract; end;
Next, add the Iterate method to our TPersonList class:
TPersonList = class(TObjectList) public constructor Create; procedure Iterate(pVisitor: TPersonVisitor); end;
The implementation of this method will be as follows:
procedure TPersonList.Iterate(pVisitor: TPersonVisitor); var i: integer; begin for i := 0 to Count - 1 do pVisitor.Execute(TPerson(Items[i])); end;
The object of the implemented Visitor of the TPersonVisitor class is passed to the Iterate method, and when iterating through the list items, the specified Visitor (its execute method) is called for each of them with the TPerson instance as a parameter.
Create two implementations of the Visitor, TShowNameVisitor and TShowEmailVistor, which will perform the required work. Here is how the module interface section is replenished:
TShowNameVisitor = class(TPersonVisitor) public procedure Execute(pPerson: TPerson); override; end; TShowEmailVisitor = class(TPersonVisitor) public procedure Execute(pPerson: TPerson); override; end;
For the sake of simplicity, the implementation of the execute methods will still be one line - ShowMessage (pPerson.Name) and ShowMessage (pPerson.EMailAdrs).
And change the code for the button click handlers:
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;
Now, having solved one problem, we have created another. The iterator logic is encapsulated in a separate class; the operations performed during the iteration are wrapped in objects, which allows us to save some state information, but the code size has grown from one line (FPersonList.DoSomething (@DoShowName); up to nine lines for each button handler. What It will help us now - this is the Visitors Manager, who will take care of creating and freeing their instances.Potentially, we can provide for performing several operations with objects during their iteration, for this, the Visitors Manager will store their list and run through it at each step, you . Olnyaya only selected operations Next will clearly demonstrate the benefits of this approach, we will use the visitors to save the data in a relational database as a simple data saving operation can be carried out by three different SQL statements: CREATE, DELETE and the UPDATE.
Step 4. Further Visitor Encapsulation
Before proceeding further, we must encapsulate the logic of the Visitor’s work, separating it from the business logic of the application so as not to return to it. We have three steps to do this: create TVisited and TVisitor base classes, then base classes for a business object and a collection of business objects, then adjust our specific TPerson and TPersonList (or TPeople) classes a little so that they become the heirs of the created base classes. classes. In general terms, the structure of classes will correspond to this diagram:

The TVisitor object implements two methods: the AcceptVisitor function and the Execute procedure, into which an object of the TVisited type is passed. The TVisited object, in turn, implements the Iterate method with a parameter of type TVisitor. That is, TVisited.Iterate should call the Execute method on the transferred TVisitor object, sending a link to its own instance as a parameter, and if the instance is a collection, then the Execute method is called for each item contained in the collection. The AcceptVisitor function is necessary, since we are developing a generic system. It will be possible to pass to the Visitor, which operates only with types of TPerson, an instance of the TDog class, for example, and there must be a mechanism to prevent exceptions and access errors due to the type mismatch. The TVisited class is a successor of the TPersistent class, since we will need to implement the functions related to the use of RTTI a little later.
The interface part of the module will now be like this:
TVisited = class; TVisitor = class(TObject) protected function AcceptVisitor(pVisited: TVisited): boolean; virtual; abstract; public procedure Execute(pVisited: TVisited); virtual; abstract; end; TVisited = class(TPersistent) public procedure Iterate(pVisitor: TVisitor); virtual; end;
The methods of the abstract TVisitor class will be implemented by successors, and the general implementation of the Iterate method for TVisited is given below:
procedure TVisited.Iterate(pVisitor: TVisitor); begin pVisitor.Execute(self); end;
In this case, the method is declared virtual in order to be able to override it in successors.
Step 5. Creating a shared business object and collection
Our framework needs two more base classes: to define a business object and a collection of such objects. Let's call them TtiObject and TtiObjectList. The interface of the first of them:
TtiObject = class(TVisited) public constructor Create; virtual; end;
In the future, in the development process, we will complicate this class, but for the current task it is enough to have only one virtual constructor with the possibility of overriding it in the heirs.
We plan to generate the TtiObjectList class from TVisited in order to use behavior in methods that has already been implemented by the ancestor (there are also other reasons for this inheritance, which will be discussed in their place). In addition, nothing prohibits the use of
interfaces (interfaces) instead of abstract classes.
The interface part of the TtiObjectList class will be as follows:
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;
As you can see, the container itself with object elements is located in a protected section and will not be accessible to clients of this class. The most important part of the class is the implementation of the override Iterate. If in the base class the method was simply called pVisitor.Execute (self), here the implementation is associated with enumerating the list:
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;
The implementation of other class methods takes up one line of code without taking into account the automatically placed inherited expressions:
Create: FList := TObjectList.Create; Destroy: FList.Free; Clear: if Assigned(FList) then FList.Clear; Add: if Assigned(FList) then FList.Add(pData);
This is an important part of the whole system. We have two base classes of business logic: TtiObject and TtiObjectList. Both have an Iterate method in which an instance of the TVisited class is passed. The iterator itself calls the TVSitor class's Execute method and passes it a reference to the object itself. This call is predefined in class behavior at the top level of inheritance. For a container class, each object stored in the list also has its Iterate method, called with a parameter of type TVisitor, that is, it is guaranteed that each specific Visitor will bypass all objects stored in the list, as well as the list itself as a container object.
Step 6. Creating a visitor manager
So, back to the problem that we ourselves drew in the third step. Since we don’t want to create and destroy instances of Visitors every time, the development of the Manager will be the solution. It should perform two main tasks: manage the list of Visitors (which are registered as such in the initialization section of individual modules) and start their execution when receiving the appropriate command from the client.
To implement the manager, we will add our module with three additional classes: The TVisClassRef, TVisMapping and TtiVisitorManager.
TVisClassRef = class of TVisitor;
TVisClassRef is a reference type and indicates the name of a particular class - a descendant of TVisitor. The meaning of using a reference type is as follows: when the base Execute method with a signature is called
procedure Execute(const pData: TVisited; const pVisClass: TVisClassRef),
inside this method can use an expression like lVisitor: = pVisClass.Create to create an instance of a specific Visitor, not knowing initially about its type. That is, any class - a descendant of TVisitor can be dynamically created within the same Execute method by passing the name of its class as a parameter.
The second class TVisMapping is a simple data structure with two properties: a reference to the TVisClassRef type and a Command string property. The class is needed to compare the operations performed by their name (command, for example, “save”) and the Visitor class, which these commands execute. Add its code to the project:
TVisMapping = class(TObject) private FCommand: string; FVisitorClass: TVisClassRef; public property VisitorClass: TVisClassRef read FVisitorClass write FVisitorClass; property Command: string read FCommand write FCommand; end;
And the last class is TtiVisitorManager. When we register a Visitor using the Manager, an instance of the TVisMapping class is created and entered into the Manager list.
Thus, in the Manager, a list of Visitors is created with the matching of a string command, upon receipt of which they will be executed. The class interface is added to the 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;
Its key methods are RegisterVisitor and Execute. The first is usually called in the initialization section of the module, which describes the Visitor class, and looks like this:
initialization gTIOPFManager.VisitorManager.RegisterVisitor('show', TShowNameVisitor); gTIOPFManager.VisitorManager.RegisterVisitor('show', TShowEMailAdrsVisitor);
The code for the method itself will be as follows:
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;
It is not difficult to notice that this code is very similar to the Pascal implementation of the
Factory pattern.
Another important method, Execute, takes two parameters: a command that identifies the Visitor to be executed or their group, as well as a data object, the Iterate method of which will be called with passing a reference to the instance of the desired Visitor. The full code for the Execute method is shown below:
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;
Thus, to launch one team of two previously registered Visitors, we need only one line of code:
gTIOPFManager.VisitorManager.Execute('show', FPeople);
Next, we will complement our project so that you can call such commands:
Step 7. Adjustment of business logic classes
Adding an ancestor of the TtiObject and TtiObjectList classes to our TPerson and TPeople business objects allows us to encapsulate the iterator logic in the base class and not touch it anymore, in addition, it becomes possible to transfer data objects to the Visitors Manager.
The new container class declaration will look like this:
TPeople = class(TtiObjectList);
In fact, the TPeople class does not even have to implement anything itself. Theoretically, we could do without the TPeople declaration at all and store objects in an instance of the TtiObjectList class, but since we plan to write Visitors that process only TPeople instances, we need this class. In the AcceptVisitor function, the following verification will be performed:
Result := pVisited is TPeople.
For the TPerson class, we add the ancestor TtiObject, and move the two existing properties to the published scope, since in the future we will need to work through RTTI with these properties. Much later, this will significantly reduce the code mapping objects and records in a relational database:
TPerson = class(TtiObject) private FEMailAdrs: string; FName: string; published property Name: string read FName write FName; property EMailAdrs: string read FEMailAdrs write FEMailAdrs; end;
Step 8. Create a prototype view
Remark In the original article, the GUI was based on components that the author of tiOPF made for the convenience of working with its framework in delphi. It was similar to DB Aware components, which were standard controls such as tags, input fields, checkboxes, lists, etc., but they were associated with certain properties of tiObject objects as well as data display components were associated with fields of database tables. Over time, the framework author marked the packages with these visual components as obsolete and undesirable to use. Instead, he proposes creating a link between visual components and class properties using the Mediator design pattern. This template is the second most important in the entire framework architecture.
The description of the Intermediary is taken up by the author in a separate article, comparable in size to this manual, so here I am offering my simplified version as a GUI.Rename button 1 on the project form to “show command”, and either leave button 2 for now without a handler, or call “save command” right away. Throw a memo-component on the form and place all the elements to your taste.Add a Visitor class that will implement the "show" command:Interface - TShowVisitor = class(TVisitor) protected function AcceptVisitor(pVisited: TVisited): boolean; override; public procedure Execute(pVisited: TVisited); override; end;
And the implementation is 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 verifies that the object being passed is an instance of TPerson, since the Visitor has to execute a command with such objects only. If the type matches, the command is executed and a line with the properties of the object is added to the text field.Auxiliary actions for the performance of the code will be as follows. Add two properties to the description of the form itself in the private section: FPeople of type TPeople and VM of type TtiVisitorManager. In the form creation event handler, we need to initiate these properties, as well as register the Visitor with the “show” command: FPeople := TPeople.Create; FillPeople; VM := TtiVisitorManager.Create; VM.RegisterVisitor('show',TShowVisitor);
FilPeople is also an auxiliary procedure that fills the list with three objects, its code is taken from the previous list constructor. Do not forget also to destroy all created objects. In this case, in the form closing handler, we write FPeople.Free and VM.Free.And now - bams! - handler of the first button: Memo1.Clear; VM.Execute('show',FPeople);
Agree, it is already much more fun. And do not swear at the jumble of all classes in a single module. At the very end of the manual, we will rake these debris.Step 9. The base class of the Visitor, working with text files
At this stage we will create the base class of the Visitor, who knows how to work with text files. In object pascal, there are three ways to work with files: old procedures from the time of the first pascal (like AssignFile and ReadLn), work through streams (TStringStream or TFileStream), and using the TStringList object.If the first method is very outdated, then the second and third are a good alternative based on the PLO. At the same time, work with threads additionally gives such advantages as the ability to compress and encrypt data, but the line-by-line reading and writing to the stream is a kind of redundancy in our example. For simplicity, we will choose TStringList, which has two simple methods - LoadFromFile and SaveToFile. But remember that with large files these methods will significantly slow down, so the stream will be the best choice for them.Interface base class TVisFile: TVisFile = class(TVisitor) protected FList: TStringList; FFileName: TFileName; public constructor Create; virtual; destructor Destroy; override; end;
And the implementation of the constructor and destructor: 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;
The value of the FFileName property will be assigned in the constructors of the descendants of this base class (just do not use hardcoding, which we will arrange here, as the main programming style after!). The class diagram of Visitors working with files is as follows:
In accordance with the diagram below, we create two descendants of the TVisFile base class: TVisTXTFile and TVisCSVFile. One will work with * .csv files in which data fields are separated by a symbol (comma), the second one - with text files in which individual data fields will be of a fixed length in a line. For these classes, we redefine only constructors as follows: constructor TVisCSVFile.Create; begin FFileName := 'contacts.csv'; inherited Create; end; constructor TVisTXTFile.Create; begin FFileName := 'contacts.txt'; inherited Create; end.
Step 10. Add a Text File Visitor
Here we will add two specific Visitors, one will read the text file, the second will write to it. The reader must override the AcceptVisitor and Execute base class methods. AcceptVisitor verifies that the TPeople class object is passed to the Visitor: Result := pVisited is TPeople;
The implementation of execute looks like this: procedure TVisTXtRead.Execute(pVisited: TVisited); var i: integer; lData: TPerson; begin if not AcceptVisitor(pVisited) then Exit;
The visitor first clears the list of the TPeople object passed to it by the parameter, then reads the strings from its TStringList object, into which the file contents are loaded, creates a TPerson object on each line and adds it to the list of the TPeople container. For simplicity, the name and emailadrs properties in a text file are separated by spaces.The record visitor implements the reverse operation. Its constructor (redefined) clears the internal TStringList (i.e., performs the FList.Clear operation; it is mandatory after inherited), AcceptVisitor checks that an object of the TPerson class is passed, which is not an error, but an important difference from the same method of the Visitor read. It would seem easier to implement a record in the same way - scan all the objects in the container, add them to the StringList and then save it to a file. All this was true, if we really talked about the final writing of data to a file, but we plan to perform data mapping in a relational database, this should be remembered. And in this case, we should execute SQL code only for those objects that have been changed (created, deleted or edited). That is why before the Visitor performs an operation on an objecthe has to check its type: Result := pVisited is Tperson;
The execute method simply adds a string formatted with the specified rule to the internal StringList: first, the contents of the name property of the passed object, padded with spaces to 20 characters, then the contents of the emaiadrs property: 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;
Step 11. Add a CSV File Handler
Visitors of reading and writing are similar in almost all their colleagues from the TXT classes except for the way in which the final line of the file is formatted: in the CSV standard, property values ​​are separated by commas. To read the lines and parse it into properties, we use the ExtractDelimited function from the strutil module, and the write is done by simple string concatenation: 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;
All we have to do is register new Visitors in the Manager and check the functionality of the application. In the form creation handler, add the following code: VM.RegisterVisitor('readTXT', TVisTXTRead); VM.RegisterVisitor('saveTXT',TVisTXTSave); VM.RegisterVisitor('readCSV',TVisCSVRead); VM.RegisterVisitor('saveCSV',TVisCSVSave);
Let's do the necessary buttons on the form and assign the appropriate handlers to them:
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;
Additional file formats for saving data are realized by simply adding relevant Visitors and registering them in the Manager. And pay attention to the following: we deliberately called the teams differently, that is, saveTXT and saveCSV. If both Visitors compare one save command, then both of them will start at the same command, check it yourself.Step 12. Final cleaning code
For greater beauty and cleanliness of the code, as well as for the preparation of the project for the further development of interaction with the DBMS, we will distribute our classes in different modules in accordance with the logic and their purpose. In the end, we should have the following structure of modules in the project folder, which allows you to do without circular dependencies between them (when assembling, arrange the necessary modules yourself in the uses sections):Module
| Function
| Classes
|
tivisitor.pas
| Base Visitor and Manager Template Classes
| TVisitor TVisited TVisMapping TtiVisitorManager
|
tiobject.pas
| Basic Business Logic Classes
| TtiObject TtiObjectList
|
people_BOM.pas
| Specific Business Logic Classes
| TPerson TPeople
|
people_SRV.pas
| Specific Interaction Classes
| TVisFile TVisTXTFile TVisCSVFile TVisCSVSave TVisCSVRead TVisTXTSave TVisTXTRead
|
Conclusion
In this article, we looked at the problem of iterating a collection or a list of objects that can have different types. We used the Visitor template proposed by GoF to optimally implement two different ways of mapping data from objects into files of different formats. In this case, different methods can be performed on one team thanks to the creation of the Visitor Manager. In the end, simple and illustrative examples, analyzed in the article, will help us further develop in a similar way a system of mapping objects into a relational database.The source code archive is here.