How the tiOPF framework for delphi / lazarus works. Visitor Template
- Tutorial
From translator
There are two reasons why I decided to translate several materials on a framework developed twenty years ago for the not-so-popular programming environment:
1. Several years ago, when I knew many of the delights of working with Entity Framework as ORM for the .Net platform, I searched in vain for analogues Lazarus environments and generally for freepascal.
Surprisingly, good ORMs are missing for her. All that was found then was an open-source project called tiOPF , developed back in the late 90s for delphi, later ported to freepascal. However, this framework is fundamentally different from the usual look of large and thick ORMs.
Визуальные способы проектирования объектов (в Entity — model first) и сопоставления объектов с полями таблиц реляционной базы данных (в Entity — database first) в tiOPF отсутствуют. Разработчик сам позиционирует этот факт как один из недостатков проекта, однако в качестве достоинства предлагает полную ориентацию именно на объектную бизнес-модель, стоит лишь один раз похардкодить…
It was at the level of the proposed hardcoding that I had problems. At that time, I was not very well versed in those paradigms and methods that the framework developer used in full and mentioned in the documentation several times per paragraph (design patterns of the visitor, linker, observer, several levels of abstraction for DBMS independence, etc. .). My large project working with the 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,
After reading enough literature on the principles of the correct design of databases and information systems, including the study of templates, as well as getting to know the Entity Framework, I decided to do a full refactoring of both the database itself and my application. And if I completely coped with the first task, then for the implementation of the second there were two roads going in different directions: either completely go to study .net, C # and the Entity Framework, or find a suitable ORM for the familiar Lazarus system. There was also a third, first, inconspicuous bicycle trail - to write ORM to fit your needs yourself, but this is not the point now.
The source code of the framework is not commented much, but the developers nevertheless prepared (apparently in the initial period of development) a certain amount of documentation. All of it, of course, is English-speaking, and experience shows that, despite the abundance of code, diagrams, and template programming phrases, many Russian-speaking programmers are still poorly oriented in English-language documentation. Not always and not everyone has a desire to train the ability to understand the English technical text without the need for the mind to translate it into Russian.
In addition, repeated proofreading of the text for translation allows you to see what I missed when I first met the documentation, I did not understand it completely or incorrectly. That is, this is for himself an opportunity to better learn the framework under study.
2. In the documentation, the author intentionally or not skips some pieces of code, probably obvious in his opinion. Due to the limitations of its writing, the documentation uses outdated mechanisms and objects as examples, deleted or no longer used in new versions of the framework (didn’t I say that it itself continues to develop?). Also, when I repeated the developed examples myself, I found some errors that should be fixed. Therefore, in places I allowed myself not only to translate the text, but also to supplement or revise it so that it remains relevant, and the examples were working.
I want to start the translation of materials from an article by Peter Henrikson about the first “whale” on which the whole framework stands - the Visitor template. Original text posted here .
Visitor and tiOPF template
The purpose of this article is to introduce the Visitor template, the use of which is one of the main concepts of the tiOPF (TechInsite Object Persistence Framework) framework. We will consider the problem in detail, after analyzing alternative solutions before using the Visitor. In the process of developing our own visitor concept, we will face another challenge: the need to iterate through all the objects in the collection. This issue will also be studied.
The main task is to come up with a generalized way to perform a set of related methods on some objects in 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.
The necessary level of training
The reader should be familiar with the object pascal and master the basic principles of object-oriented programming.
Example business task in this article
As an example, we will develop an address book that allows you to create records of people and their contact information. With the increase in possible ways of communication between people, the application should flexibly allow you to add such methods without significant code processing (I remember once having finished processing the code to add a phone number, I immediately needed to process it again to add email). We need to provide two categories of addresses: real, such as home address, postal, work, and electronic: landline phone, fax, mobile, email, 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 something like this:

In the context menu of the tree, you can choose to add / remove a person’s or company’s contact, and right-click on the contact data list to open a dialog for editing them, delete or add data.
Data can be saved in various forms, and in the future we will consider how to use this template to implement this feature.
Before the start
We will start with a simple collection of objects - a list of people who in turn have two properties - name (Name) and address (EmailAdrs). To begin with, the list will be filled with data in the constructor, and subsequently it will be loaded from a file or database. Of course, this is a very simplified example, but it is enough to fully implement the Visitor template.
Create a new application and add two classes of the interface section of the main module: TPersonList (inherited from TObjectList and requires the connection of the contnrs module in uses) 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 create three TPerson objects and add to the list:
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;
First, we will go through the list and perform two operations on each element of the list. The operations are similar, but not the same: a simple ShowMessage call to display the contents of the Name and EmailAdrs properties of TPerson objects. Add two buttons to the form and name them something like this:

In the preferred scope of your form, you should also add a property (or just a field) FPersonList of type TPersonList (if the type is declared below the form, either change the order or make a preliminary type declaration), and in the onCreate event handler, the constructor call:
FPersonList := TPersonList.Create;
To properly free memory in the onClose event handler of the form, this object must be destroyed:
FPersonList.Free.
Step 1. Hardcode Iteration
To show names from TPerson objects, add the following code to the onClick event handler of the first button:
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 shoals of this code:
- two methods that do almost the same thing. All the difference is only in the name of the property of the object that they show;
- the iteration will be tedious, especially when you are forced to write a similar loop in a hundred places in the code;
- a hard cast to TPerson is fraught with exceptional situations. What if there is an instance of TAnimal in the list without an address property? There is no mechanism to stop the error and defend against it in this code.
Let's figure out how to improve the code by introducing an abstraction: we pass the iterator code to the parent class.
Step 2. Abstracting the iterator
So, we want to move the iterator logic to the base class. The list iterator itself is very simple:
for i := 0 to FList.Count - 1 do
// что-то сделать с элементом списка…
It sounds like we're planning on using an Iterator template . From the book on Gang-of-Four design patterns book , it is known that the Iterator can be external and internal. When using an external iterator, the client explicitly controls the traversal by calling the Next method (for example, the enumeration of TCollection elements is controlled by the First, Next, Last methods). We will use the internal iterator here, since it is easier to implement tree traversal with its help, which is our goal. We will add the Iterate method to our list class and will pass a callback method to it, which 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 a procedural type TDoSomethingToAPerson, which takes one parameter of type TPerson. The procedural type allows you to use the method as a parameter of another method, that is, implement 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 themselves will be passed as a parameter to the general iterator. Finally, the type declaration section should look like this:
{ 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;
Now, to perform the necessary actions on the list items, we need to do two things. Firstly, to determine the necessary operations using methods that have the signature specified by TDoSomethingToAPerson, and secondly, to write DoSomething calls with the transfer of 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. We now have three levels of abstractions in our code. A general iterator is a method of a class that implements a collection of objects. Business logic (so far just endless message output through ShowMessage) is placed separately. At the presentation (graphical interface) level, the business logic is called in one line.
It is easy to imagine how a call to ShowMessage can be replaced with code that saves our data from TPerson in a relational database using the 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 connection to the database. In our request, the connection to the database is carried out through some global gAppDatabase object. But where will it be located and how to work? In addition, we are tormented at each step of the iterator to create TQuery objects, configure the connection, execute the query and do not forget to free the 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 a callback
Passing the object to the iterator method of the base class will solve the problem of state maintenance. We will create an abstract Visitor class TPersonVisitor with a single Execute method and pass the object to this method as a parameter. The abstract Visitor interface is presented 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;
An object of the implemented Visitor of the TPersonVisitor class is passed to the Iterate method, and when iterating through the list items for each of them, the specified Visitor (its execute method) is called with the TPerson instance as a parameter.
Let's create two implementations of the Visitor - TShowNameVisitor and TShowEmailVistor, which will perform the required work. Here's how to replenish the module interfaces section:
{ TShowNameVisitor }
TShowNameVisitor = class(TPersonVisitor)
public
procedure Execute(pPerson: TPerson); override;
end;
{ TShowEmailVisitor }
TShowEmailVisitor = class(TPersonVisitor)
public
procedure Execute(pPerson: TPerson); override;
end;
For simplicity's sake, the implementation of the execute methods on them will still be a single 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;
Теперь же мы, решив одну проблему, создали себе другую. Логика итератора инкапсулирована в отдельном классе; операции, выполняемые при итерации, завернуты в объекты, что позволяет нам сохранять какую-то информацию о состоянии, но при этом размер кода вырос с одной строчки (FPersonList.DoSomething(@DoShowName); до девяти строк на каждый обработчик кнопок. То, что нам поможет теперь — это Менеджер Посетителей, который будет заботиться о создании и освобождении их экземпляров. Потенциально мы можем предусмотреть выполнение нескольких операций с объектами при их итерации, для этого Менеджер Посетителей будет хранить их список и пробегаться по нему при каждом шаге, выполняя только выбранные операции. Дальше будет наглядно продемонстрирована польза такого подхода, когда мы будем использовать Посетителей для сохранения данных в реляционной базе, поскольку простая операция сохранения данных может осуществляться тремя разными операторами SQL: CREATE, DELETE и UPDATE.
Step 4. Further Encapsulation of the Visitor
Before moving on, we must encapsulate the logic of the Visitor's work, separating it from the business logic of the application so that it does not return to it. It will take us three steps to do this: create the base classes TVisited and TVisitor, then the base classes for the business object and the collection of business objects, then slightly adjust our specific classes TPerson and TPersonList (or TPeople) so that they become heirs to the created base classes. In general terms, the structure of the classes will correspond to such a diagram:

The TVisitor object implements two methods: the AcceptVisitor function and the Execute procedure, into which the TVisited type object is passed. The TVisited object, in turn, implements the Iterate method with a parameter of type TVisitor. That is, TVisited.Iterate must 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, the Execute method is called for each element in the collection. The AcceptVisitor function is necessary since we are developing a generalized system. It will be possible to pass to the Visitor, who operates only with TPerson types, an instance of the TDog class, for example, and there must be a mechanism to prevent exceptions and access errors due to type mismatch. The TVisited class is the descendant of the TPersistent class,
The interface part of the module will now be like this:
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;
The methods of the TVisitor abstract class will be implemented by the heirs, and the general implementation of the Iterate method for TVisited is given below:
procedure TVisited.Iterate(pVisitor: TVisitor);
begin
pVisitor.Execute(self);
end;
At the same time, the method is declared virtual for the possibility of its override in the heirs.
Step 5. Create a shared business object and collection
Our framework needs two more base classes: to define a business object and a collection of such objects. Call them TtiObject and TtiObjectList. The interface of the first of them:
TtiObject = class(TVisited)
public
constructor Create; virtual;
end;
Later in the development process, we will complicate this class, but for the current task, only one virtual constructor with the possibility of overriding it in the heirs is enough.
We plan to generate the TtiObjectList class from TVisited in order to use the behavior in methods that has already been implemented by the ancestor (there are also other reasons for this inheritance that will be discussed in its 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 the object elements is located in the protected section and will not be available to customers of this class. The most important part of the class is the implementation of the overridden Iterate method. If in the base class the method simply called pVisitor.Execute (self), then here the implementation is connected 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 one line of code without taking into account 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 basic classes of business logic: TtiObject and TtiObjectList. Both have an Iterate method to which an instance of the TVisited class is passed. The iterator itself calls the Execute method of the TVisitor class and passes it a reference to the object itself. This call is predefined in the 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 the 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 on the third step. Since we do not want to create and destroy copies 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 run them when they receive the appropriate command from the client.
To implement the manager, we will supplement 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),
internally, this method can use an expression like lVisitor: = pVisClass.Create to create an instance of a specific Visitor, without first knowing about its type. That is, any class - a descendant of TVisitor can be dynamically created inside the same Execute method when 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 type TVisClassRef and a string property Command. A class is needed to compare the operations performed by their name (a 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 the Visitor using the Manager, an instance of the TVisMapping class is created, which is entered in the Manager list.
Thus, in the Manager, a list of Visitors is created with a string command matching, 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 one is usually called in the initialization section of the module, which describes the Visitor class, and looks something like this:
initialization
gTIOPFManager.VisitorManager.RegisterVisitor('show', TShowNameVisitor);
gTIOPFManager.VisitorManager.RegisterVisitor('show', TShowEMailAdrsVisitor);
The code of 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 template .
Another important Execute method takes two parameters: the command by which the Visitor or group of them to be identified will be identified, as well as the data object whose Iterate method will be called with a link to the instance of the desired Visitor. The complete code for the Execute method is given 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 run two previously registered Visitors with one team, we need only one line of code:
gTIOPFManager.VisitorManager.Execute('show', FPeople);
Next, we will supplement our project so that you can call similar commands:
// для чтения данных из хранилища
gTIOPFManager.VisitorManager.Execute('read', FPeople);
// для записи данных в хранилище
gTIOPFManager.VisitorManager.Execute('save', FPeople).
Step 7. Adjusting Business Logic Classes
Adding the ancestor of the TtiObject and TtiObjectList classes for our TPerson and TPeople business objects will allow us to encapsulate the iterator logic in the base class and not touch it anymore, in addition, it becomes possible to transfer objects with data to the Visitor 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. Theoretically, we could do without a 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 checks will be performed:
Result := pVisited is TPeople.
For the TPerson class, we add the TtiObject ancestor, and move the two existing properties to the published scope, since in the future we will need to work through RTTI with these properties. It is this much later that will significantly reduce the code involved in 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
Comment. In the original article, the GUI was based on the components that the author of tiOPF made for the convenience of working with his framework in delphi. These were analogues of DB Aware components, which were standard controls such as labels, input fields, checkboxes, lists, etc., but were associated with certain properties of tiObject objects in the same way that data display components were associated with fields in database tables. Over time, the author of the framework marked packages with these visual components as obsolete and undesirable to use. In return, he suggests creating a link between visual components and class properties using the Mediator design pattern. This template is the second most important in the entire architecture of the framework. The author’s description of the intermediary takes a separate article,
Rename button 1 on the project form to “show command”, and button 2 either leave it without a handler for now, or immediately name it “save command”. 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 transferred is an instance of TPerson, because the Visitor should only execute the command with such objects. If the type matches, the command is executed and a line with object properties is added to the text field.
Supporting actions for the health 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 filling a list with three objects; its code is taken from the previous list constructor. Do not forget to destroy all created objects. In this case, we write FPeople.Free and VM.Free in the form closing handler.
And now - bams! - handler of the first button:
Memo1.Clear;
VM.Execute('show',FPeople);
Agree, so much more fun. And do not swear at the hash of all classes in one module. At the very end of the manual, we will rake these rubble.
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. There are three ways to work with files in the object pascal: 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 OOP. At the same time, working with streams additionally provides such benefits as the ability to compress and encrypt data, but line-by-line reading and writing to a stream is a kind of redundancy in our example. For simplicity, we will choose a 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 optimal choice for them.
TVisFile base class interface:
TVisFile = class(TVisitor)
protected
FList: TStringList;
FFileName: TFileName;
public
constructor Create; virtual;
destructor Destroy; override;
end;
And the constructor and destructor implementation:
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 the hardcoding, which we will arrange here, as the main programming style after!). The diagram of the Visitor classes 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 - with text files in which individual data fields will be of a fixed length per line. For these classes, we only redefine the 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 the Visitor-handler of text files
Here we will add two specific Visitors, one will read a text file, the second will write to it. The reading visitor 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 execute implementation is as follows:
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;
The visitor first clears the list of the TPeople object passed to him by the parameter, then reads the lines from his TStringList object, into which the contents of the file are loaded, creates a TPerson object on each line and adds it to the TPeople container list. For simplicity, the name and emailadrs properties in the text file are separated by spaces.
The record visitor implements the inverse operation. Its constructor (overridden) clears the internal TStringList (i.e., performs the FList.Clear operation; it is mandatory after inherited), AcceptVisitor checks that the TPerson class object is passed, which is not an error, but an important difference from the same Visitor reading method. It would seem easier to implement the recording in the same way - scan all the container objects, add them to a StringList and then save it to a file. All this was so if we really were talking about the final writing of data to a file, however we plan to map data to a relational database, this should be remembered. And in this case, we should execute the SQL code only for those objects that have been changed (created, deleted or edited). That is why before the Visitor performs an operation on the object,
Result := pVisited is Tperson;
The execute method simply adds to the internal StringList a string formatted with the specified rule: first, the contents of the name property of the passed object, padded with spaces up 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 the Visitor-handler of CSV files
Visitors to reading and writing are similar in almost all their colleagues from TXT classes, except for the way to format the final line of a file: in the CSV standard, property values are separated by commas. To read lines and parse them into properties, we use the ExtractDelimited function from the strutils module, and writing is performed by simply concatenating the lines:
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 that remains for us is to register new Visitors in the Manager and check the operation 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);
Dock the necessary buttons on the form and assign them the appropriate handlers:

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 implemented by simply adding the appropriate Visitors and registering them in the Manager. And pay attention to the following: we intentionally named the commands differently, that is, saveTXT and saveCSV. If both Visitors match one save command, then both of them will start on the same command, check it yourself.
Step 12. Final code cleanup
For the greater beauty and purity of the code, as well as for preparing a project for the further development of interaction with the DBMS, we will distribute our classes into 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 us to do without a circular relationship between them (when assembling yourself, arrange the necessary modules in uses sections):
Module | Function | Classes |
tivisitor.pas | Base classes of the Visitor and Manager template | TVisitor TVisited TVisMapping TtiVisitorManager |
tiobject.pas | Base Business Logic Classes | TtiObject TtiObjectList |
people_BOM.pas | Specific Business Logic Classes | TPerson TPeople |
people_SRV.pas | Concrete classes responsible for the interaction | TVisFile TVisTXTFile TVisCSVFile TVisCSVSave TVisCSVRead TVisTXTSave TVisTXTRead |
Conclusion
In this article, we examined the problem of iterating over a collection or 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 to files of different formats. At the same time, different methods can be performed by one team due to the creation of the Visitor Manager. Ultimately, the simple and illustrative examples discussed in the article will help us further develop a similar system for mapping objects to a relational database.
Archive with source code of examples - here