TDataSetProxy は、従来の Delphi データセット コンポーネントのラッパー コンポーネントです。これにより、任意のデータセットを偽のデータセット (メモリ内テーブル) に置き換えることができます。プロキシを使用すると、ビジネス クラスをデータセットから分離できます。この分離は、ビジネス コードを自動テスト ハーネス (単体テスト) に入れる必要がある場合に役立ちます。
インスピレーション。このアイデアは、Martin Fowler が著書『 Patterns of Enterprise Application Architecture』で定義した Proxy GoF パターンと Active Record パターンに基づいています。
DataSet プロキシ パターンは、ビジネス ロジックの抽出時に役立ちます。これは、従来の高度に結合されたプロジェクトを改善する場合に特に役立ちます。運用コードが SQL データと SQL 接続に依存している場合、そのようなコードの単体テストを作成するのは非常に困難です。
データセットをプロキシに置き換えることで、運用コードの SQL データセットとテスト プロジェクトのメモリ データセットの両方を促進できる新しい抽象化レベルが導入されます。プロキシには、従来のデータセットとよく似たインターフェイス (メソッド リスト) があり、簡単な移行に役立ちます。偽のデータセットを使用すると、データベースに接続せずに実稼働コードを検証 (アサート) できます。
DataSet プロキシと 2 つの関連プロジェクト (DataSet ジェネレーター、Delphi コマンド パターン) を併用すると、開発者は安全なリファクタリングを使用して単体テストを導入する機会が得られます。
データセット プロキシは一時的なソリューションであり、コードをテストでカバーした後、エンジニアはより高度なリファクタリングを適用して、コードを分離したり、より構成可能で再利用しやすくしたりできます。これらのリファクタリングの 1 つとして、プロキシは DAO オブジェクトまたはモデル データ構造によって安全に置き換えることができます。
コードと品質の向上とともに、開発者はよりクリーンなコードを記述する方法や、テストファーストアプローチを使用して作業を改善する方法を学びます。
支援プロジェクト:
プロジェクト | GitHub リポジトリ |
---|---|
データセットジェネレータ | https://github.com/参考までに/dataset-generator |
プロジェクトには、基本クラスTDataSetProxy
のソース コードと 2 つの異なるタイプのプロキシ ジェネレーターが含まれています。
src/Comp.Generator.DataProxy.pas
TDataSetProxy
から継承されたプロキシ クラスを含むユニットtools/generator-app
コンポーネントTDataProxyGenerator
、エンジニアが運用コード内の既存のデータセットのプロキシを生成する場合に役立ちます。これは 2 ステップの簡単なタスクです: (1) コンポーネント ユニットを uses セクションに追加する、(2) データセットを使用してコードを検索し、ジェネレーター メソッドを呼び出す:
現在の製品コード:
aBooksDataSet := fDBConnection.ConstructSQLDataSet(
aOwner, APPSQL_SelectBooks);
dbgridBooks.DataSource.Dataset := aBooksDataSet;
挿入されたジェネレーター コード:
TDataProxyGenerator.SaveToFile( ' ../../src/Proxy.Books ' ,
aBooksDataSet, ' TBookProxy ' );
Generator App for FireDAC は、主にデモ目的で作成された代替ツールです。実際には、このツールを使用すると、コンポーネント ジェネレーターを直接使用する場合よりも有用性が低くなる可能性があります。ジェネレーター アプリは、コーチングとトレーニングの目的に特化しています。詳細については、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 を 1 回クローンしただけです。欠落しているメソッドを一度追加してこのメソッドのセットを簡単に拡張したり、新しい独自のメソッドを構築したりできます。このプロキシ メソッドは、 Append
、 Edit
、 Cancel
、 Delete
、 Close
、 Post
、 RecordCount
、 First
、 Last
、 Eof
、 Next
、 Prior
、 EnableControls
、 DisableControls
、 Locate
、 Lookup
、 Refresh
などです。ドキュメントとこのメソッドの使用法は、 TDataSet
クラスの標準の Delphi ドキュメントと同じです。
残りのTDataSetProxy
メソッドは、プロキシ セットアップ メソッド (構成) とプロキシ ヘルパー メソッド (従来のデータセット機能の拡張) の 2 つのグループに分類できます。
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
コンポーネントの現在のリリースには、例として実装されたヘルパー メソッドが 1 つだけ含まれています。開発者は、チームのコーディング慣行に従ってこのコレクションを拡張できます。プロキシ クラスを拡張するには、継承を使用することをお勧めします。既存の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 | テキストは各コードのインデントに使用されます。デフォルト値は 2 つのスペースです |
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 コマンドを入力し、アクティブ プロパティを設定し、すべての DB 対応コントロールをクエリに接続すれば完了です...ほぼ完了、ほぼ完了ですが、実際にはアプリケーションを提供する準備にはほど遠いです。
このシンプルな視覚的パターンを使用すると、開発者は SQL サーバー データを非常に迅速に公開および変更できます。実際には、最初は簡単そうに見えても、後者は困難になります。そのうちにエンジニアは、ビジネス フローをデフラグし、プレゼンテーション、構成、ドメイン コードを混合しながら、より多くのデータセットとイベントを作成します。プロジェクトはますます混乱し、結合していきます。数年後、マネージャーや開発者はそのようなプロジェクトをコントロールできなくなります。計画や期限は定量化できず、顧客は予期せぬ奇妙なバグに悩まされ、単純な変更には何時間もの作業が必要です。
クラシック データセットをプロキシに置き換えるには、実際に学習して検証するのに時間がかかります。このアプローチは、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
コントロールに格納されており、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
2 つの類似したクラスがあります。これらのクラスのどれが他のクラスに依存するかを決定することが重要です。 TBook
はモデル層の一部であり、データ アクセス オブジェクトについては意識する必要はありません。
procedure TForm1.LoadBooksToListBox ();
begin
ListBox1.Clear;
fProxyBooks.LoadAndValidate;
fProxyBooks.FillStringsWithBooks(ListBox1.Items);
end ;
最後に、form メソッドは見栄えが良く、明確です。これは私たちが正しい方向に進んでいることを示す良い兆候です。抽出され、データセット プロキシに移動されたコードは、以前とほぼ同じようになります。
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
を移動する必要がありました。
このプロキシには、ListBox を埋めるために使用される書籍fBookList
の内部コレクションが含まれています。その時点で、変更の数を減らすためにこのコードをデータセット プロキシ クラスに移動しましたが、適切なクラスに移動する必要があります。
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
in ( Data.Mock.Book.pas
ユニット)