TDataSetProxy 是经典 Delphi 数据集组件的包装组件。它允许用假数据集(内存表)替换任何数据集。代理可用于将业务类与数据集分离,当业务代码需要放入自动化测试工具(单元测试)时,这种分离很有帮助。
灵感。想法基于 Proxy GoF 模式和 Active Record 模式,由 Martin Fowler 在《企业应用程序架构模式》一书中定义
数据集代理模式在业务逻辑提取过程中很有帮助。这对于改进遗留的、高度耦合的项目特别有用。当生产代码依赖于 SQL 数据和 SQL 连接时,为此类代码编写单元测试确实很困难。
用代理替换数据集引入了新的抽象级别,可以促进生产代码中的 SQL 数据集和测试项目中的内存数据集。 Proxy 具有与经典数据集非常相似的界面(方法列表),这有助于轻松迁移。假数据集将允许在不连接到数据库的情况下验证(断言)生产代码。
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 ' );
FireDAC 的 Generator App是主要用于演示目的的替代工具。在实践中,使用此工具可能不如直接使用组件生成器有用。 Generator 应用程序专用于辅导和培训目的。有关更多信息,请查看:FireDAC 的 Generator App - 用户指南。
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
等。文档和此方法的用法与TDataSet
类的标准 Delphi 文档相同。
其余的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
后生成的代码将存储在可通过Code
属性访问的内部TStringList
中。请参阅下面的示例代码:
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 方法不仅效率较低,而且对于开发人员、经理和客户来说甚至是危险的。
在 Delphi 中使用 RDBMS(SQL 服务器)看起来非常高效且简单。开发人员删除一个Query
组件,输入 SQL 命令,设置 Active 属性,连接所有数据库感知控件进行查询,然后您就完成了……几乎完成了,几乎但实际上还远远没有准备好交付应用程序。
使用这个简单的可视化模式,开发人员可以非常快速地公开和修改 SQL Server 数据。事实上,看似简单的事情,到了后面就变得具有挑战性。随着时间的推移,工程师创建越来越多的数据集和事件,对业务流程进行碎片整理并混合表示、配置和域代码。项目变得越来越混乱和耦合。几年后,经理和开发人员失去了对此类项目的控制:计划和截止日期无法量化,客户正在与意想不到的奇怪错误作斗争,简单的更改需要大量的工作时间。
用代理替换经典数据集需要一些时间来学习和实际验证。对于 Delphi 开发人员来说,这种方法可能看起来有点奇怪,但很容易采用和学习。在管理层的激励和高级工程师的指导下,团队将更快地采用代码提取并用代理技术替换数据集。
这里定义的代理方法是一种简单且安全的重构技术,专用于以 EDP(事件驱动编程)方式构建的经典 VCL 应用程序。以演进方式使用此解决方案可以提取业务代码的小但重要部分并通过单元测试覆盖。一段时间后,有了更好的安全网(单元测试覆盖率),工程师可以用 OOP DAO 交换代理,并使用高级重构和架构模式进一步改进代码。
现代化过程包括以下步骤:
查看显示使用 TDataSetProxy 的旧 VCL 项目的迁移路径的示例。我们将从以下形式定义的经典方法开始:
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 的目标是改善这种状态并将业务逻辑与可视化分离。
此方法使用fdqBook
TFDQuery 从 SQL 数据库加载数据。为每一行创建一个TBook
类的对象,其字段填充数据集值并进行验证。由于TBook
对象存储在TListBox
控件中,该控件也拥有它们,因此此方法必须首先释放它们。
我们用代理对象替换数据集。此外,我们通过使用函数式ForEach
方法更改经典的while-not-eof
循环来实现代码现代化。同时,我们引入了一种更安全的访问字段值的变体。可以将此阶段分为 3 个单独的阶段,但对于本文,我们需要保持内容紧凑。
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
in ( Data.Proxy.Book.pas
单元)function CreateMockTableBook
in( Data.Mock.Book.pas
单元)