Как устроен фреймворк tiOPF для delphi/lazarus. Шаблон «Посетитель» +3


От переводчика


Есть две причины, по которым я взялся перевести несколько материалов по разработанному двадцать лет назад для не самой популярной ныне среды программирования фреймворку:

1. Несколько лет назад я, познав многие прелести работы с Entity Framework как ORM для платформы .Net, тщетно искал аналоги для среды Lazarus и в общем для freepascal.
Удивительно, но хорошие ORM для нее отсутствуют. Всё, что тогда удалось найти — open-source проект под названием tiOPF, разработанный еще в конце 90-х годов для delphi, позже портированный под freepascal. Однако этот фреймворк коренным образом отличается от привычного вида больших и толстых ORM.

Визуальные способы проектирования объектов (в Entity — model first) и сопоставления объектов с полями таблиц реляционной базы данных (в Entity — database first) в tiOPF отсутствуют. Разработчик сам позиционирует этот факт как один из недостатков проекта, однако в качестве достоинства предлагает полную ориентацию именно на объектную бизнес-модель, стоит лишь один раз похардкодить…

Именно на уровне предлагаемого хардкодинга у меня и возникли проблемы. На тот момент я не слишком хорошо ориентировался в тех парадигмах и методах, которые разработчик фреймворка использовал по полной программе и упоминал в документации по нескольку раз на абзац (шаблоны проектирования посетитель, компоновщик, наблюдатель, несколько уровней абстракции для СУБД-независимости и т.д.). Мой большой проект, работающий с базой данных, в тот период был полностью ориентирован на визуальные компоненты Lazarus и способ работы с базами данных, предлагаемый визуальной средой, как следствие — тонны одинакового кода: три таблицы в самой БД с практически одинаковой структурой и однородными данными, по три одинаковые формы для просмотра, три одинаковые формы для редактирования, три одинаковые формы для отчетов и все остальное из топа рубрики «как не стоит проектировать ПО».

Прочитав достаточно литературы по принципам правильного проектирования баз данных и информационных систем, включая изучение шаблонов, а также познакомившись с Entity Framework, я решил сделать полный рефакторинг как самой базы данных, так и своего приложения. И если с первой задачей я вполне справился, то для реализации второй были две дороги, уходящие в разные стороны: либо полностью уйти на изучение .net, C# и Entity Framework, либо найти подходящую ORM для привычной системы Lazarus. Был и третий сначала незаметный велосипедный трейл — написать ORM под свои нужды самому, но не об этом сейчас речь.

Исходный код фреймворка мало комментирован, однако разработчики все же подготовили (видимо в первоначальный период разработки) некоторый объем документации. Вся она, разумеется, англоязычная, а опыт показывает, что, несмотря на обилие кода, диаграмм и шаблонных программистских фраз, многие русскоговорящие программисты до сих пор плохо ориентируются в англоязычной документации. Не всегда и не у всех имеется желание тренировать способность понимания английского технического текста без необходимости в уме переводить его на русский.

Кроме того повторная вычитка текста для перевода позволяет увидеть то, что я пропустил при первом знакомстве с документацией, понял не до конца или неправильно. То есть это и для самого себя возможность лучше усвоить изучаемый фреймворк.

2. В документации автор умышленно или нет пропускает некоторые куски кода, наверное, очевидные на его взгляд. В связи с давностью её написания документация использует в качестве примеров устаревшие механизмы и объекты, удаленные или более не используемые в новых версиях фреймворка (а я разве не говорил, что сам-то он продолжает развиваться?). Также при самостоятельном повторении разрабатываемых примеров я обнаружил и некоторые ошибки, которые следовало бы исправить. Поэтому местами я позволил себе не только перевести текст, но и дополнить или переработать его для того, чтобы он оставался актуальным, а примеры были рабочими.

Начать перевод материалов хочу со статьи Петера Хенриксона о первом «ките», на котором стоит весь фреймворк — шаблон Посетитель (Visitor). Оригинальный текст размещен здесь.

Шаблон Посетитель и tiOPF


Цель настоящей статьи — познакомить с шаблоном Посетитель, использование которого является одной из основных концепций фреймворка tiOPF (TechInsite Object Persistence Framework). Мы подробно рассмотрим проблему, предварительно проанализировав альтернативные способы решения перед тем, как использовать Посетителя. В процессе разработки собственной концепции Посетителя мы столкнемся с еще одной задачей: необходимостью итерации всех объектов коллекции. Данный вопрос также будет изучен.

Основная задача состоит в том, чтобы придумать обобщенный способ выполнения набора связанных методов над некоторыми объектами коллекции. Выполняемые методы могут отличаться в зависимости от внутреннего состояния объектов. Мы можем не выполнять методы вообще, а можем выполнить несколько методов над одними и теми же объектами.

Необходимый уровень подготовки


Читатель должен быть хорошо знаком с объектным паскалем и владеть основными принципами объектно-ориентированного программирования.

Пример бизнес-задачи в данной статье


В качестве примера мы разработаем адресную книгу, позволяющую создавать записи о людях и их контактную информацию. С увеличением возможных способов связи между людьми, приложение должно гибко позволять добавлять такие способы без существенной переработки кода (помню, как однажды только закончив переработку кода для добавления телефонного номера, сразу же понадобилось снова перерабатывать его для добавления электронной почты). Нам нужно предусмотреть две категории адресов: реальные, такие как домашний адрес, почтовый, рабочий, и электронные: стационарный телефон, факс, мобильный, электронная почта, сайт.

На уровне представления наше приложение должно иметь вид, похожий на Проводник/Outlook, то есть предполагается использовать стандартные компоненты такие как TreeView и ListView. Приложение должно работать быстро и не производить впечатление громоздкого клиент-серверного ПО.

Примерно так может выглядеть приложение:



В контекстном меню дерева можно выбирать добавление/удаление контакта человека или компании, а правый клик на списке контактных данных — вызывать диалог их редактирования, удалять или добавлять данные.

Данные могут сохраняться в различной форме, и в дальнейшем мы рассмотрим как с помощью шаблона Адаптер реализовать эту возможность.

Перед началом


Мы начнем работу с простой коллекции объектов — списка людей, которые в свою очередь имеют два свойства — имя (Name) и адрес (EmailAdrs). Для начала список будет заполняться данными в конструкторе, а в дальнейшем — загружаться из файла или базы данных. Конечно, это очень упрощенный пример, однако его достаточно для полной реализации шаблона Посетитель.

Создайте новое приложение и добавьте два класса секции interface главного модуля: TPersonList (наследуется от TObjectList и требует подключения в uses модуля contnrs) и TPerson (наследуется от 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;

В конструкторе TPersonList мы создадим три объекта TPerson и добавим в список:

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;

Для начала мы пройдемся по списку и выполним две операции над каждым элементом списка. Операции схожи, но не одинаковы: простой вызов ShowMessage с выводом содержимого свойств Name и EmailAdrs объектов TPerson. Добавьте две кнопки на форму и назовите их примерно так:



В предпочтительную область видимости вашей формы также следует добавить свойство (или просто поле) FPersonList типа TPersonList (если при этом тип объявляется ниже формы, то либо поменяйте порядок, либо сделайте предварительное объявление типа), а в обработчик события onCreate вызов конструктора:

FPersonList := TPersonList.Create;

Для правильного освобождения памяти в обработчике события onClose формы — этот объект должен быть уничтожен:

FPersonList.Free.

Шаг 1. Хардкодинг итерации


Чтобы показать имена из объектов TPerson, добавьте следующий код в обработчик события onClick первой кнопки:

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

Для второй кнопки код обработчика будет следующий:

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

Вот очевидные косяки этого кода:

  • два метода, которые делают практически одно и то же. Вся разница — лишь в названии свойства объекта, которое они показывают;
  • итерация будет утомительной, особенно когда вы вынуждены будете написать аналогичный цикл в сотне мест кода;
  • жесткое приведение типа к TPerson чревато возникновением исключительных ситуаций. Что если в списке затесался экземпляр TAnimal без свойства адреса? Механизм остановить ошибку и защититься от неё в этом коде отсутствует.

Давайте прикинем, как можно улучшить код путем введения абстракции: передадим код итератора родительскому классу.

Шаг 2. Абстрагирование итератора


Итак, мы хотим вынести логику итератора в базовый класс. Сам по себе итератор списка очень прост:

for i := 0 to FList.Count - 1 do
  // что-то сделать с элементом списка…

Звучит так, будто мы планируем воспользоваться шаблоном Итератор. Из книги о паттернах проектирования банды четырех (Gang-of-Four design patterns book) известно, что Итератор бывает внешним и внутренним. При использовании внешнего итератора процессом обхода явно управляет клиент путем вызова метода Next (к примеру, перебор элементов TCollection управляется методами First, Next, Last). Мы здесь будем использовать внутренний итератор, поскольку с его помощью легче реализовать обход дерева, что и является нашей целью. К нашему классу списка мы добавим метод Iterate и будем передавать в него callback-метод, который должен выполняться над каждым элементом списка. Callback в объектном паскале объявляется как процедурный тип, у нас будет, к примеру, TDoSomethingToAPerson.

Итак, мы объявляем процедурный тип TDoSomethingToAPerson, который принимает один параметр типа TPerson. Процедурный тип позволяет использовать метод в качестве параметра другого метода, то есть реализовать callback. Таким способом мы создадим два метода, один из которых будет показывать свойство Name объекта, а другой — свойство EmailAdrs, а сами они будут передаваться как параметр для общего итератора. Окончательно секция объявления типов должна выглядеть следующим образом:

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

Теперь, чтобы выполнить нужные действия над элементами списка, нам нужно сделать две вещи. Во-первых, определить нужные операции с помощью методов, имеющих сигнатуру, заданную TDoSomethingToAPerson, а во-вторых, написать вызовы DoSomething с передачей в качестве параметра указателей на эти методы. В секции описания формы добавим два объявления:

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

В реализации этих методов укажем:

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

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

Код же для обработчиков нажатия кнопок изменим следующим образом:

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

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

Уже лучше. Сейчас у нас в коде три уровня абстракций. Общий итератор является методом класса, реализующего коллекцию объектов. Бизнес-логика (пока просто бесконечный вывод сообщений через ShowMessage) размещена обособленно. На уровне представления (графического интерфейса) вызов бизнес-логики осуществляется одной строкой.

Легко представить, как вызов ShowMessage может быть заменен на код, сохраняющий с помощью SQL-запроса объекта TQuery наши данные из TPerson в реляционной базе. К примеру, так:

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;

Кстати, это вводит новую проблему поддержания соединения с базой данных. У нас в запросе подключение к базе осуществляется через некий глобальный объект gAppDatabase. Но где он будет располагаться и как работать? Кроме того, мы замучаемся при каждом шаге итератора создавать объекты TQuery, настраивать соединение, исполнять запрос и не забывать освобождать память. Лучше бы этот код завернуть в класс, инкапсулирующий логику создания и выполнения SQL-запросов, а также настройки и поддержания соединения с базой данных.

Шаг 3. Передача объекта вместо передачи указателя на callback


Передача объекта в метод-итератор базового класса решит проблему поддержки состояния. Мы создадим абстрактный класс Посетителя TPersonVisitor с единственным методом Execute и передадим объект в этот метод как параметр. Интерфейс абстрактного Посетителя представлен ниже:

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

Дальше добавляем метод Iterate в наш класс TPersonList:

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

Реализация этого метода будет следующей:

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

В метод Iterate передается объект реализованного Посетителя класса TPersonVisitor, и при переборе элементов списка для каждого из них вызывается указанный Посетитель (его метод execute) с экземпляром TPerson в качестве параметра.

Создадим две реализации Посетителя — TShowNameVisitor и TShowEmailVistor, которые будут выполнять требуемую работу. Вот как пополнится секция интерфейсов модуля:

{ TShowNameVisitor }

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

  { TShowEmailVisitor }

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

Для целей простоты реализация методов execute у них будет по-прежнему представлять собой одно строку — ShowMessage(pPerson.Name) и ShowMessage(pPerson.EMailAdrs).

И изменим код обработчиков нажатия кнопок:

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.

Шаг 4. Дальнейшая инкапсуляция Посетителя


Перед тем, как двигаться дальше, мы должны инкапсулировать логику работы Посетителя, отделив ее от бизнес-логики приложения так, чтобы к ней не возвращаться. У нас уйдет на это три шага: создадим базовые классы TVisited и TVisitor, после — базовые классы для бизнес-объекта и коллекции бизнес-объектов, затем немного подкорректируем наши конкретные классы TPerson и TPersonList (или TPeople) так, чтобы они стали наследниками созданных базовых классов. В общих чертах структура классов будет соответствовать такой диаграмме:



Объект TVisitor реализует два метода: функцию AcceptVisitor и процедуру Execute, в которые передается объект типа TVisited. Объект TVisited в свою очередь реализует метод Iterate с параметром типа TVisitor. То есть, TVisited.Iterate должен вызывать метод Execute у переданного объекта TVisitor, в качестве параметра отправляя ссылку на свой собственный экземпляр, а если экземпляр представляет собой коллекцию, то метод Execute вызывается для каждого содержащегося в коллекции элемента. Функция AcceptVisitor необходима, так как мы разрабатываем обобщенную систему. Можно будет передать Посетителю, который оперирует только с типами TPerson, экземпляр класса TDog, к примеру, и должен иметься механизм предотвращения исключительных ситуаций и ошибок доступа из-за несоответствия типов. Класс TVisited является наследником класса TPersistent, поскольку немного позже нам потребуется реализация функций, связанных с использованием RTTI.

Интерфейсная часть модуля будет теперь такой:

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;

Методы абстрактного класса TVisitor будут реализовываться наследниками, а общая реализация метода Iterate для TVisited приведена ниже:

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

При этом метод объявлен виртуальным для возможности его переопределения (override) в наследниках.

Шаг 5. Создание общего бизнес-объекта и коллекции


Нашему фреймворку нужны еще два базовых класса: для определения бизнес-объекта и коллекции таких объектов. Назовем их TtiObject и TtiObjectList. Интерфейс первого из них:

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

В дальнейшем в процессе разработки мы усложним этот класс, но для текущей задачи достаточно лишь одного виртуального конструктора с возможностью его переопределения в наследниках.

Класс TtiObjectList мы планируем породить от TVisited с целью использования поведения в методах, которое уже реализовано предком (также для этого наследования есть и другие причины, о которых будет сказано в своем месте). Кроме того ничто не запрещает использовать интерфейсы (interfaces) вместо абстрактных классов.

Интерфейсная часть класса TtiObjectList будет следующей:

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;

Как видно сам контейнер с элементами-объектами расположен в защищенной секции и не будет доступен клиентам данного класса. Наиболее важная часть класса — реализация переопределенного метода Iterate. Если в базовом классе метод вызывал просто pVisitor.Execute(self), то здесь реализация связана с перебором списка:

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;

Реализация других методов класса занимает по одной строчке кода без учета автоматически расставляемых inherited выражений:

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

Это важная часть всей системы. У нас есть два базовых класса бизнес-логики: TtiObject и TtiObjectList. Оба имеют метод Iterate, в который передается экземпляр класса TVisited. Сам итератор вызывает метод Execute класса TVisitor и передает ему ссылку на сам объект. Этот вызов предопределен в поведении класса на верхнем уровне наследования. Для класса-контейнера каждый сохраненный в списке объект также имеет свой метод Iterate, вызываемый с параметром типа TVisitor, то есть гарантируется, что каждый конкретный Посетитель обойдет все объекты, сохраненные в списке, а также сам список как объект-контейнер.

Шаг 6. Создание менеджера посетителей


Так, вернемся к проблеме, которую мы сами себе нарисовали на третьем шаге. Поскольку мы не слишком сильно хотим каждый раз создавать и уничтожать экземпляры Посетителей, решением будет разработка Менеджера. Он должен выполнять две основные задачи: управлять списком Посетителей (которые регистрируются в качестве таковых в секции инициализации отдельных модулей) и запускать их выполнение при получении соответствующей команды от клиента.
Для реализации менеджера мы дополним наш модуль тремя дополнительными классами: The TVisClassRef, TVisMapping и TtiVisitorManager.

TVisClassRef = class of TVisitor;

TVisClassRef представляет собой ссылочный тип и указывает на название конкретного класса — потомка TVisitor. Смысл использования ссылочного типа в следующем: когда будет вызван базовый метод Execute с сигнатурой

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

внутри этот метод может использовать выражение вроде lVisitor := pVisClass.Create для создания экземпляра конкретного Посетителя, не зная изначально о его типе. То есть, любой класс — потомок TVisitor может быть динамически создан внутри одного и того же метода Execute при передаче в качестве параметра наименования его класса.

Второй класс TVisMapping представляет собой простую структуру данных с двумя свойствами: ссылка на тип TVisClassRef и строковое свойство Command. Класс нужен для сопоставления выполняемых операций по их наименованию (команде, к примеру «save») и классу Посетителя, которые эти команды исполняют. Добавим его код в проект:

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

И последний класс — TtiVisitorManager. Когда мы регистрируем Посетителя с помощью Менеджера, создается экземпляр класса TVisMapping, который заносится в список Менеджера.
Таким образом, в Менеджере создается список Посетителей с сопоставлением строковой команды, при поступлении которой они будут выполняться. Интерфейс класса добавляем в модуль:

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;

Ключевые его методы — это RegisterVisitor и Execute. Первый как правило вызывается в секции initialization модуля, в котором описывается класс Посетителя, и выглядит примерно так:

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

Код самого метода будет следующим:

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;

Не трудно заметить, что этот код весьма похож на паскалевскую реализацию шаблона Фабрики.

Другой важный метод Execute принимает два параметра: команду, по которой будет идентифицироваться подлежащий выполнению Посетитель или их группа, а также объект данных, метод Iterate которого будет вызываться с передачей ссылки на экземпляр нужного Посетителя. Полный код метода Execute приведен ниже:

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;

Таким образом, для запуска одной командой двух ранее зарегистрированных Посетителей, нам требуется лишь одна строчка кода:

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

Дальше мы дополним наш проект, чтобы можно было вызывать подобные команды:

// для чтения данных из хранилища
gTIOPFManager.VisitorManager.Execute('read', FPeople);
// для записи данных в хранилище
gTIOPFManager.VisitorManager.Execute('save', FPeople).

Шаг 7. Корректировка классов бизнес-логики


Добавление для наших бизнес-объектов TPerson и TPeople предка классов TtiObject и TtiObjectList позволит инкапсулировать логику итератора в базовом классе и больше ее не трогать, кроме того становится возможной передача объектов с данными в Менеджер Посетителей.

Новое объявление класса-контейнера будет выглядеть так:

TPeople = class(TtiObjectList);

На самом деле класс TPeople даже не должен сам ничего реализовывать. Теоретически мы могли бы обойтись вообще без объявления TPeople и хранить объекты в экземпляре класса TtiObjectList, но поскольку мы планируем написать Посетителей, обрабатывающих только экземпляры TPeople, нам нужен этот класс. В функции AcceptVisitor при этом будет производиться проверка следующего характера:

Result := pVisited is TPeople.

Для класса TPerson мы добавляем предка TtiObject, а два имеющихся свойства перемещаем в область видимости published, поскольку в дальнейшем нам потребуется работать через RTTI с этими свойствами. Именно это много позже позволит значительно сократить код, занимающийся маппингом объектов и записей в реляционной базе данных:

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

Шаг 8. Создаем прототип представления


Замечание. В оригинальной статье GUI был основан на компонентах, которые автор tiOPF сделал для удобства работы со своим фреймворком в delphi. Это была аналоги DB Aware компонентов, которые представляли собой стандартные элементы управления типа меток, полей ввода, чекбоксов, список и т.д., но при этом связывались с определенными свойствами объектов tiObject также, как компоненты отображения данных связывались с полями таблиц БД. Со временем автор фреймворка пометил пакеты с этими визуальными компонентами как устаревшие и нежелательные к использованию. Взамен он предлагает создавать связь между визуальными компонентами и свойствами классов с помощью шаблона проектирования Посредник (Mediator). Этот шаблон является вторым наиболее важным во всей архитектуре фреймворка. Описание Посредника у автора занимает отдельную статью, сопоставимую по объему с данным руководством, поэтому я здесь в качестве GUI предлагаю свой упрощенный вариант.

Переименуйте кнопку 1 на форме проекта в «команда show», а кнопку 2 либо оставьте пока без обработчика, либо сразу назовите «команда save». Киньте на форму memo-компонент и разместите все элементы на свой вкус.

Добавьте класс Посетителя, который будет реализовывать команду «show»:

Интерфейс —

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

И реализация —
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 проверяется, что передаваемый объект является экземпляром TPerson, поскольку Посетитель должен выполнять команду только с такими объектами. Если тип соответствует, то команда выполняется и в текстовое поле добавляется строчка со свойствами объекта.

Вспомогательные действия для работоспособности кода будут следующими. Добавьте в описание самой формы в private секцию два свойства: FPeople типа TPeople и VM типа TtiVisitorManager. В обработчике события создания формы нам требуется инициировать эти свойства, а также зарегистрировать Посетителя с командой «show»:

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

FilPeople — также вспомогательная процедура, заполняющая список тремя объектами, ее код взят из предыдущего конструктора списка. Не забывайте также уничтожать все создаваемые объекты. В данном случае в обработчике закрытия формы мы пишем FPeople.Free и VM.Free.

И теперь — бамс! — обработчик первой кнопки:

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

Согласитесь, так уже гораздо веселее. И не ругайтесь на мешанину всех классов в одном модуле. В самом конце руководства мы разгребем эти завалы.

Шаг 9. Базовый класс Посетителя, работающего с текстовыми файлами


На этом этапе мы создадим базовый класс Посетителя, умеющего работать с текстовыми файлами. В объектном паскале есть три пути для работы с файлами: старинные процедуры со времен первого паскаля (наподобие AssignFile и ReadLn), работа через потоки (TStringStream или TFileStream) и использование объекта TStringList.

Если первый способ сильно устарел, то второй и третий представляют собой хорошую альтернативу, основанную на ООП. При этом работа с потоками дополнительно дает такие плюшки, как возможность сжатия и шифрования данных, но построчное считывание и запись в поток представляет собой некую избыточность в нашем примере. Для простоты мы выберем TStringList, у которого есть два простых метода — LoadFromFile и SaveToFile. Но помните, что с файлами большого объема эти методы будут значительно тормозить, так что оптимальным выбором для них станет именно поток.

Интерфейс базового класса TVisFile:

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

И реализация конструктора и деструктора:

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;

Значение свойству FFileName будет присваиваться в конструкторах потомков этого базового класса (только не используйте хардкодинг, который мы здесь устроим, как основной стиль программирования после!). Диаграмма классов Посетителей, работающих с файлами, представляется такой:



В соответствии с диаграммой далее мы создаем двух потомков базового класса TVisFile: TVisTXTFile и TVisCSVFile. Один будет работать с файлами *.csv, в которых поля данных разделены символом (запятой), второй — с текстовыми файлами, в которых отдельные поля данных будут фиксированной длины в строке. Для этих классов мы переопределяем лишь конструкторы следующим образом:

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

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

Шаг 10. Добавляем Посетителя-обработчика текстовых файлов


Здесь мы добавим два конкретных Посетителя, один будет читать текстовый файл, второй записывать в него. Посетитель чтения должен переопределить методы базового класса AcceptVisitor и Execute. AcceptVisitor проверяет, что Посетителю передается объект класса TPeople:

Result := pVisited is TPeople;

Реализация execute выглядит следующим образом:

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;

Посетитель сначала очищает список объекта TPeople, переданного ему параметром, затем считывает строки из своего объекта TStringList, в который загружено содержимое файла, создает на каждую строку объект TPerson и добавляет его в список контейнера TPeople. Для простоты свойства name и emailadrs в текстовом файле разделяются пробелами.

Посетитель записи реализует обратную операцию. Конструктор его (переопределенный) очищает внутренний TStringList (т.е. выполняет операцию FList.Clear; при этом обязательно после inherited), AcceptVisitor проверяет, что передан объект класса TPerson, что является не ошибкой, а важным отличием от того же метода Посетителя чтения. Казалось бы, проще реализовать запись аналогично — просканировать все объекты контейнера, добавить их в StringList и потом сохранить его в файл. Все это было так, если бы у нас действительно шла речь о конечной записи данных в файл, однако мы планируем выполнять маппинг данных в реляционную базу, об этом следует помнить. И в этом случае нам следует выполнять SQL код только для тех объектов, которые были изменены (созданы, удалены или отредактированы). Именно поэтому прежде чем Посетитель выполнит операцию над объектом, он должен проверить соответствие его типа:

Result := pVisited is Tperson;

Метод execute просто добавляет во внутренний StringList строку, отформатированную с заданным правилом: сначала содержимое свойства name переданного объекта, дополненное пробелами до размера 20 символов, затем содержимое свойства 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;

Шаг 11. Добавляем Посетителя-обработчика CSV-файлов


Посетители чтения и записи аналогичны практически во всем своим коллегам из TXT классов за исключением способа форматирования конечной строки файла: в стандарте CSV значения свойств разделяются запятыми. Для чтения строк и разбора её на свойства мы используем функцию ExtractDelimited из модуля strutils, а запись выполняется простой конкатенацией строк:

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;

Всё, что нам остается, это зарегистрировать новых Посетителей в Менеджере и проверить работоспособность приложения. В обработчике создания формы добавим следующий код:

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

На форму докинем нужных кнопок и назначим им соответствующие обработчики:



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;

Дополнительные форматы файлов для сохранения данных реализуются простым добавлением соответствующих Посетителей и регистрацией их в Менеджере. И еще обратите внимание на следующее: мы намеренно назвали команды по-разному, то есть saveTXT и saveCSV. Если же обоим Посетителям сопоставить  одну команду save, то они оба запустятся по одной команде, проверьте это самостоятельно.

Шаг 12. Окончательная чистка кода


Для пущей красоты и чистоты кода, а также для подготовки проекта для дальнейшей разработки взаимодействия с СУБД, разнесем наши классы по разным модулям в соответствии с логикой и их назначением. В конечном итоге у нас в папке проекта должна быть следующая структура модулей, которая позволяет обойтись без круговой зависимости между ними (при сборке самостоятельно расставьте нужные модули в uses секциях):

Модуль
Функция
Классы
tivisitor.pas
Базовые классы шаблона Посетителя и Менеджера
TVisitor
TVisited
TVisMapping
TtiVisitorManager
tiobject.pas
Базовые классы бизнес-логики
TtiObject
TtiObjectList
people_BOM.pas
Конкретные классы бизнес-логики
TPerson
TPeople
people_SRV.pas
Конкретные классы, отвечающие за взаимодействие
TVisFile
TVisTXTFile
TVisCSVFile
TVisCSVSave
TVisCSVRead
TVisTXTSave
TVisTXTRead

Заключение


В этой статье мы рассмотрели проблему итерации коллекции или списка объектов, которые могут иметь различные типы. Мы использовали шаблон Посетитель, предложенный GoF, для оптимальной реализации двух разных способов маппинга данных из объектов в файлы разного формата. При этом разные способы могут выполняться по одной команде благодаря созданию Менеджера Посетителей. В конечном итоге простые и наглядные примеры, разобранные в статье, помогут нам дальше разработать аналогичным образом систему маппинга объектов в реляционную базу данных.

Архив с исходным кодом примеров — здесь




К сожалению, не доступен сервер mySQL