TDataSetProxy — это компонент-оболочка для классического компонента набора данных Delphi. Это позволяет заменить любой набор данных поддельным набором данных (таблицей в памяти). Прокси-сервер можно использовать для отделения бизнес-класса от наборов данных. Это разделение полезно, когда бизнес-код необходимо поместить в автоматизированное тестовое оборудование (юнит-тесты).
Вдохновение . Идея основана на шаблоне Proxy GoF и шаблоне Active Record, определенных Мартином Фаулером в книге «Шаблоны архитектуры корпоративных приложений».
Шаблон DataSet Proxy полезен при извлечении бизнес-логики. Это может быть особенно полезно для улучшения устаревших, сильно связанных проектов. Когда рабочий код зависит от данных SQL и соединения SQL, писать модульные тесты для такого кода действительно сложно.
Замена набора данных прокси-серверами представляет новый уровень абстракции, который может облегчить как наборы данных SQL в рабочем коде, так и наборы данных памяти в тестовом проекте. Интерфейс прокси (список методов) очень похож на классический набор данных, что упрощает миграцию. Поддельные наборы данных позволят проверять (утверждать) рабочий код без подключения к базе данных.
DataSet Proxy вместе с двумя сопутствующими проектами (DataSet Generator, Delphi Command Pattern) дает разработчикам возможность внедрять модульные тесты с безопасным рефакторингом.
Прокси набора данных — это временное решение, и после покрытия кода тестами инженеры могут применить более продвинутый рефакторинг: разъединить код или сделать его более компонуемым и пригодным для повторного использования. В качестве одного из этих рефакторингов прокси-сервер можно безопасно заменить объектом DAO или структурами данных модели.
Вместе с улучшением кода и качества разработчики узнают, как писать более чистый код или как использовать подход «сначала тестирование» и работать лучше.
Поддерживающий проект:
Проект | Репозиторий GitHub |
---|---|
Генератор набора данных | https://github.com/bogdanpolak/dataset-generator |
Проект включает в себя исходный код базового класса TDataSetProxy
и два разных типа генераторов прокси:
src/Comp.Generator.DataProxy.pas
TDataSetProxy
tools/generator-app
Компонент TDataProxyGenerator
полезен, когда инженер хочет создать прокси для выхода из набора данных в рабочем коде. Это простая задача, состоящая из двух шагов: (1) добавить модуль компонента в раздел «Использует», (2) найти код, используя набор данных и метод генератора вызовов:
Текущий производственный код:
aBooksDataSet := fDBConnection.ConstructSQLDataSet(
aOwner, APPSQL_SelectBooks);
dbgridBooks.DataSource.Dataset := aBooksDataSet;
Введенный код генератора:
TDataProxyGenerator.SaveToFile( ' ../../src/Proxy.Books ' ,
aBooksDataSet, ' TBookProxy ' );
Приложение Generator для FireDAC — альтернативный инструмент, созданный в основном для демонстрационных целей. На практике использование этого инструмента может оказаться менее полезным, чем непосредственное использование генератора компонентов. Приложение Generator предназначено для целей коучинга и обучения. Для получения дополнительной информации проверьте: Приложение Generator для FireDAC — Руководство пользователя.
type
TBookProxy = class (TDatasetProxy)
private
fISBN :TWideStringField;
fTitle :TWideStringField;
fReleseDate :TDateField;
fPages :TIntegerField;
fPrice :TBCDField;
protected
procedure ConnectFields ; override;
public
property ISBN :TWideStringField read fISBN;
property Title :TWideStringField read fTitle;
property ReleseDate :TDateField read fReleseDate;
property Pages :TIntegerField read fPages;
property Price :TBCDField read fPrice;
end ;
procedure TBookProxy.ConnectFields ;
begin
Assert(fDataSet.Fields.Count = 5 );
fISBN := fDataSet.FieldByName( ' ISBN ' );
fTitle := fDataSet.FieldByName( ' Title ' );
fReleseDate := fDataSet.FieldByName( ' ReleseDate ' );
fPages := fDataSet.FieldByName( ' Pages ' );
fPrice := fDataSet.FieldByName( ' Price ' );
end ;
Компонент DataSetProxy — это прокси-класс, методы которого практически идентичны классическому компоненту TDataSet. Разработчик может легко заменить любой компонент DataSet этим прокси-сервером, внося в рабочий код лишь небольшие изменения с минимальным риском. С точки зрения производственного кода изменения небольшие и не очень важные, но с точки зрения тестирования это фундаментальное изменение, поскольку разработчик может перенастроить прокси для использования облегченного набора данных в памяти.
Большинство методов TDataSetProxy
являются просто клонами TDataSet. Вы можете легко расширить набор этих методов, добавив один раз недостающие или создать новые уникальные. К этим прокси-методам относятся: Append
, Edit
, Cancel
, Delete
, Close
, Post
, RecordCount
, First
, Last
, Eof
, Next
, Prior
, EnableControls
, DisableControls
, Locate
, Lookup
, Refresh
и другие. Документация и использование этих методов такие же, как и стандартная документация Delphi для класса TDataSet
.
Остальные методы TDataSetProxy
можно разделить на две группы: методы настройки прокси (конфигурация) и вспомогательные методы прокси (расширение функциональности классического набора данных).
procedure TDataModule1.OnCreate (Sender: TObject);
begin
fOrdersProxy := TOrdersProxy.Create(fOwner);
fOrdersDataSource := fOrdersProxy.ConstructDataSource;
end ;
procedure TDataModule1.InitOrders (aYear, aMonth: word);
begin
fOrdersProxy.WithFiredacSQL( FDConnection1,
' SELECT OrderID, CustomerID, OrderDate, Freight ' +
' FROM {id Orders} WHERE OrderDate between ' +
' :StartDate and :EndDate ' ,
[ GetMonthStart(aYear, aMonth),
GetMonthEnd(aYear, aMonth) ],
[ftDate, ftDate])
.Open;
fOrdersInitialized := True;
end ;
procedure TDataModule1.InitOrders (aDataSet: TDataSet);
begin
fOrdersProxy.WithDataSet(aDataSet).Open;
fOrdersInitialized := True;
end ;
Текущая версия компонента TDataSetProxy
содержит только один вспомогательный метод, который реализован в качестве примера. Разработчики могут расширять эту коллекцию в соответствии с практикой командного кодирования. Предлагается расширить прокси-класс с помощью наследования. Пример использования существующего вспомогательного метода ForEach
:
function TDataModule.CalculateTotalOrders ( const aCustomerID: string): Currency;
begin
Result := 0 ;
fOrdersProxy.ForEach(procedure
begin
if fOrdersProxy.CustomerID. Value = aCustomerID then
Result := Result + fOrdersProxy.GetTotalOrderValue;
end ;
end ;
Comp.Generator.DataProxy.pas
SaveToFile
SaveToClipboard
Execute
Code
DataSet
GeneratorMode
DataSetAccess
FieldNamingStyle
NameOfUnit
NameOfClass
IndentationText
Вариант | Ценности | Описание |
---|---|---|
GeneratorMode | ( pgmClass , pgmUnit ) | Генерирует только заголовок и реализацию класса или весь модуль с классом. |
NameOfUnit | String | Имя сгенерированного модуля, используемое для создания заголовка модуля. |
NameOfClass | String | Имя сгенерированного прокси-класса |
FieldNamingStyle | ( fnsUpperCaseF , fnsLowerCaseF ) | Определяет, как будут называться поля класса: с использованием суффикса F в верхнем регистре или в нижнем регистре. |
IndentationText | String | Текст используется для каждого отступа кода, значение по умолчанию — два пробела. |
DataSetAccess | ( dsaNoAccess , dsaGenComment , dsaFullAccess ) | Определяет доступ к внутреннему набору данных прокси: полный доступ = для доступа создается свойство только для чтения. Опция «Нет доступа» используется по умолчанию и рекомендуется. |
Чтобы сгенерировать класс poxy, вы можете использовать метод Execute, но перед его вызовом вам необходимо настроить все параметры и свойства DataSet
. После вызова Execute
сгенерированный код будет сохранен во внутреннем TStringList
доступном через свойство Code
. См. пример кода ниже:
aProxyGenerator:= TDataProxyGenerator.Create(Self);
try
aProxyGenerator.DataSet := fdqEmployees;
aProxyGenerator.NameOfUnit := ' Proxy.Employee ' ;
aProxyGenerator.NameOfClass := ' TEmployeeProxy ' ;
aProxyGenerator.IndentationText := ' ' ;
aProxyGenerator.Execute;
Memo1.Lines := aProxyGenerator.Code;
finally
aProxyGenerator.Free;
end ;
Гораздо более простой и компактный способ создания прокси-классов — использовать методы класса-генератора: SaveToFile
или SaveToClipboard
. Его имена достаточно значимы, чтобы понять их функциональность. SaveToFile генерирует весь модуль и записывает его в файл, а SaveToClipboard генерирует только класс и записывает в буфер обмена Windows. См. образцы ниже:
TDataProxyGenerator.SaveToFile(
' src/Proxy.Employee ' ,
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
TDataProxyGenerator.SaveToClipboard(
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
Этот проект является результатом многолетнего опыта работы нескольких команд. Эти команды обнаружили, что классический подход Delphi, основанный на событиях, не только менее продуктивен, но даже опасен для разработчиков, менеджеров и клиентов.
Работа с СУБД (SQL-серверами) в Delphi выглядит очень продуктивно и просто. Разработчик удаляет компонент Query
, вводит команду SQL, устанавливает свойство Active, подключает все элементы управления, поддерживающие БД, к запросу, и все готово... почти готово, но на самом деле еще далеко от того, чтобы быть готовым к доставке приложения.
Используя этот простой визуальный шаблон, разработчик может чрезвычайно быстро предоставлять и изменять данные SQL-сервера. На самом деле то, что на первый взгляд кажется простым, со временем становится сложным. Со временем инженеры создают все больше и больше наборов данных и событий, дефрагментируя бизнес-поток и смешивая представление, конфигурацию и код предметной области. Проект становится все более запутанным и запутанным. Через несколько лет менеджеры и разработчики теряют контроль над таким проектом: планы и сроки невозможно определить количественно, клиенты борются с неожиданными и странными ошибками, простые изменения требуют многих часов работы.
Замена классического набора данных прокси-сервером требует некоторого времени для изучения и проверки в действии. Этот подход может показаться немного странным для разработчиков Delphi, но его легко принять и изучить. Благодаря мотивации руководства и старшему инженеру, команда тренеров быстрее внедрит метод извлечения кода и замены наборов данных прокси-серверами.
Определенный здесь прокси-подход — это простой и безопасный метод рефакторинга, предназначенный для классического приложения VCL, созданного методом EDP (программирование, управляемое событиями). Используя это решение в процессе эволюции, можно извлечь небольшие, но важные части бизнес-кода и покрыть их модульными тестами. Через некоторое время, благодаря лучшей системе безопасности (охвату модульными тестами), инженеры смогут заменять прокси-серверы ООП DAO и улучшать код, используя расширенный рефакторинг и архитектурные шаблоны.
Процесс модернизации включает в себя следующие этапы:
Посмотрите пример, показывающий путь миграции устаревшего проекта VCL с использованием TDataSetProxy. Начнем с классического метода, определенного в форме:
procedure TFormMain.LoadBooksToListBox ();
var
aIndex: integer;
aBookmark: TBookmark;
aBook: TBook;
isDatePrecise: boolean;
begin
ListBox1.ItemIndex := - 1 ;
for aIndex := 0 to ListBox1.Items.Count - 1 do
ListBox1.Items.Objects[aIndex].Free;
ListBox1.Clear;
aBookmark := fdqBook.GetBookmark;
try
fdqBook.DisableControls;
try
while not fdqBook.Eof do
begin
aBook := TBook.Create;
ListBox1.AddItem(fdqBook.FieldByName( ' ISBN ' ).AsString + ' - ' +
fdqBook.FieldByName( ' Title ' ).AsString, aBook);
aBook.ISBN := fdqBook.FieldByName( ' ISBN ' ).AsString;
aBook.Authors.AddRange(BuildAuhtorsList(
fdqBook.FieldByName( ' Authors ' ).AsString));
aBook.Title := fdqBook.FieldByName( ' Title ' ).AsString;
aBook.ReleaseDate := ConvertReleaseDate(
fdqBook.FieldByName( ' ReleaseDate ' ).AsString);
aBook.Price := fdqBook.FieldByName( ' Price ' ).AsCurrency;
aBook.PriceCurrency := fdqBook.FieldByName( ' Currency ' ).AsString;
ValidateCurrency(aBook.PriceCurrency);
fdqBook.Next;
end ;
finally
fdqBook.EnableControls;
end
finally
fdqBook.FreeBookmark(aBookmark);
end ;
end ;
Уведомление! Представленное выше решение является плохой практикой, но, к сожалению, часто используется разработчиками Delphi. Цель использования TDataProxy — улучшить это состояние и отделить бизнес-логику от визуализации.
Этот метод загружает данные из базы данных SQL с помощью fdqBook
TFDQuery. Для каждой строки создается объект класса TBook
, его поля заполняются значениями набора данных и проверяются. Поскольку объекты TBook
хранятся в элементе управления TListBox
, которому они также принадлежат, этот метод должен сначала освободить их.
Мы заменяем набор данных прокси-объектом. Кроме того, мы модернизируем код, заменяя классический цикл while-not-eof
функциональным методом ForEach
. В то же время мы вводим более безопасный вариант доступа к значениям полей. Эту фазу можно разделить на три отдельных этапа, но для этой статьи нам нужно сохранить компактность контента.
procedure TFormMain.LoadBooksToListBox ();
var
aIndex: integer;
aBook: TBook;
begin
ListBox1.ItemIndex := - 1 ;
for aIndex := 0 to ListBox1.Items.Count - 1 do
ListBox1.Items.Objects[aIndex].Free;
ListBox1.Clear;
fProxyBooks.ForEach(
procedure
begin
aBook := TBook.Create;
ListBox1.AddItem(fProxyBooks.ISBN. Value + ' - ' +
fProxyBooks.Title. Value , aBook);
aBook.ISBN := fProxyBooks.ISBN. Value ;
aBook.Authors.AddRange(
BuildAuhtorsList(fProxyBooks.Authors. Value ));
aBook.Title := fProxyBooks.Title. Value ;
aBook.ReleaseDate := ConvertReleaseDate(
fProxyBooks.ReleaseDate. Value );
aBook.Price := fProxyBooks.Price.AsCurrency;
aBook.PriceCurrency := fProxyBooks.Currency. Value ;
ValidateCurrency(aBook.PriceCurrency);
end );
end ;
Код более читабелен и безопасен, но все еще находится в форме. Пришло время удалить его и отделить от всех зависимостей, чтобы обеспечить возможность тестирования.
Мы должны начать с важного архитектурного решения. На данный момент в коде у нас есть два похожих класса: TBook
, хранящий данные, и TBookProxy
их обрабатывающий. Важно решить, какой из этих классов зависит от другого. TBook
является частью уровня модели и не должен знать об объекте доступа к данным.
procedure TForm1.LoadBooksToListBox ();
begin
ListBox1.Clear;
fProxyBooks.LoadAndValidate;
fProxyBooks.FillStringsWithBooks(ListBox1.Items);
end ;
Наконец, метод формы выглядит красиво и понятно. Это хороший знак того, что мы идем в правильном направлении. Код, извлеченный и перемещенный в прокси набора данных, выглядит почти так же, как предыдущий:
procedure TBooksProxy.LoadAndValidate ;
var
aBook: TBook;
isDatePrecise: boolean;
begin
fBooksList.Clear;
ForEach(
procedure
begin
aBook := TBook.Create;
fBooksList.Add(aBook);
aBook.ISBN := ISBN. Value ;
aBook.Authors.AddRange(
BuildAuhtorsList(Authors. Value ));
aBook.Title := Title. Value ;
aBook.ReleaseDate := ConvertReleaseDate(
ReleaseDate. Value , isDatePrecise);
aBook.IsPreciseReleaseDate := isDatePrecise;
aBook.Price := Price.AsCurrency;
aBook.PriceCurrency := Currency. Value ;
ValidateCurrency(aBook.PriceCurrency);
end );
end ;
Вместе с этим кодом нам пришлось перенести все зависимые методы, отвечающие за преобразование и проверку данных: BuildAuhtorsList
, ConvertReleaseDate
и ValidateCurrency
.
Этот прокси содержит внутреннюю коллекцию книг fBookList
, которая используется для заполнения ListBox. В тот момент мы переместили этот код в прокси-класс набора данных, чтобы уменьшить количество изменений, но букву его следует переместить в соответствующий класс:
procedure TBooksProxy.FillStringsWithBooks (
aStrings: TStrings);
var
aBook: TBook;
begin
aStrings.Clear;
for aBook in fBooksList do
aStrings.AddObject(
aBook.ISBN + ' - ' + aBook.Title, aBook);
end ;
TBookProxy
в (модуль Data.Proxy.Book.pas
)function CreateMockTableBook
в (модуль Data.Mock.Book.pas
)