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
單元)