TDataSetProxy ist eine Wrapper-Komponente für die klassische Delphi-Dataset-Komponente. Es ermöglicht das Ersetzen jedes Datensatzes durch einen gefälschten Datensatz (In-Memory-Tabelle). Proxy kann verwendet werden, um eine Geschäftsklasse von Datensätzen zu trennen. Diese Trennung ist hilfreich, wenn der Geschäftscode in eine automatisierte Testumgebung (Komponententests) eingefügt werden muss.
Inspiration . Die Idee basiert auf dem Proxy-GoF-Muster und dem Active-Record-Muster, die von Martin Fowler im Buch „Patterns of Enterprise Application Architecture“ definiert wurden
Das DataSet-Proxy-Muster ist bei der Extraktion der Geschäftslogik hilfreich. Dies könnte besonders nützlich sein, um ältere, stark gekoppelte Projekte zu verbessern. Wenn Produktionscode von SQL-Daten und einer SQL-Verbindung abhängig ist, ist es wirklich schwierig, Komponententests für solchen Code zu schreiben.
Durch das Ersetzen von Datensätzen durch Proxys wird eine neue Abstraktionsebene eingeführt, die beides ermöglichen kann: SQL-Datensätze im Produktionscode und Speicherdatensätze im Testprojekt. Proxy verfügt über eine dem klassischen Datensatz sehr ähnliche Schnittstelle (Methodenliste), was die Migration erleichtert. Gefälschte Datensätze ermöglichen die Überprüfung (Bestätigung) des Produktionscodes ohne Verbindung zur Datenbank.
DataSet Proxy bietet Entwicklern zusammen mit zwei Begleitprojekten (DataSet Generator, Delphi Command Pattern) die Möglichkeit, Unit-Tests mit sicheren Refactorings einzuführen.
Der Datensatz-Proxy ist eine vorübergehende Lösung. Nachdem der Code mit den Tests abgedeckt wurde, können Ingenieure erweiterte Refactorings anwenden: Code entkoppeln oder ihn besser zusammensetzbar und wiederverwendbar machen. Als eines dieser Refactorings kann der Proxy sicher durch das DAO-Objekt oder durch die Modelldatenstrukturen ersetzt werden.
Zusammen mit Code- und Qualitätsverbesserungsentwicklern lernen sie, saubereren Code zu schreiben oder den Test-First-Ansatz zu verwenden und besser zu arbeiten.
Unterstützendes Projekt:
Projekt | GitHub-Repo |
---|---|
DataSet-Generator | https://github.com/bogdanpolak/dataset-generator |
Das Projekt umfasst Quellcode der Basisklasse TDataSetProxy
und zwei verschiedene Arten von Proxy-Generatoren:
src/Comp.Generator.DataProxy.pas
TDataSetProxy
geerbte Proxy-Klasse enthälttools/generator-app
Die Komponente TDataProxyGenerator
ist nützlich, wenn der Ingenieur einen Proxy zum Verlassen des Datensatzes im Produktionscode generieren möchte. Dies ist eine einfache Aufgabe in zwei Schritten: (1) Komponenteneinheit zum Verwendungsabschnitt hinzufügen, (2) Code mithilfe des Datensatzes finden und Generatormethode aufrufen:
Aktueller Produktionscode:
aBooksDataSet := fDBConnection.ConstructSQLDataSet(
aOwner, APPSQL_SelectBooks);
dbgridBooks.DataSource.Dataset := aBooksDataSet;
Eingefügter Generatorcode:
TDataProxyGenerator.SaveToFile( ' ../../src/Proxy.Books ' ,
aBooksDataSet, ' TBookProxy ' );
Die Generator-App für FireDAC ist ein alternatives Tool, das hauptsächlich für Demozwecke entwickelt wurde. In der Praxis kann die Verwendung dieses Tools weniger nützlich sein als die direkte Verwendung des Komponentengenerators. Die Generator-App ist für Coaching- und Schulungszwecke bestimmt. Weitere Informationen finden Sie unter: Generator-App für FireDAC – Benutzerhandbuch.
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 ;
Die DataSetProxy-Komponente ist eine Proxy-Klasse, die über fast identische Methoden wie die klassische TDataSet-Komponente verfügt. Entwickler können jede DataSet-Komponente problemlos durch diesen Proxy ersetzen, indem sie nur wenige und risikoarme Änderungen am Produktionscode vornehmen. Aus der Sicht des Produktionscodes sind die Änderungen gering und nicht sehr wichtig, aber aus der Testperspektive ist dies eine grundlegende Änderung, da der Entwickler den Proxy neu konfigurieren kann, um einen kompakten Speicherdatensatz zu verwenden.
Die meisten TDataSetProxy
-Methoden sind nur einmal Klone von TDataSet. Sie können den Satz dieser Methoden ganz einfach erweitern, indem Sie fehlende Elemente hinzufügen oder neue, einzigartige Methoden erstellen. Diese Proxy-Methoden sind: Append
, Edit
, Cancel
, Delete
, Close
, Post
, RecordCount
, First
, Last
, Eof
, Next
, Prior
, EnableControls
, DisableControls
, Locate
, Lookup
, Refresh
und andere. Die Dokumentation und die Verwendung dieser Methoden sind dieselben wie die Standard-Delphi-Dokumentation für TDataSet
-Klasse.
Die übrigen TDataSetProxy
-Methoden können in zwei Gruppen unterteilt werden: Proxy-Setup-Methoden (Konfiguration) und Proxy-Hilfsmethoden (Erweiterung der klassischen Datensatzfunktionalität).
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 ;
Die aktuelle Version der TDataSetProxy
Komponente enthält nur eine Hilfsmethode, die als Beispiel implementiert wurde. Entwickler können diese Sammlung entsprechend den Codierungspraktiken des Teams erweitern. Zur Erweiterung der Proxy-Klasse wird die Verwendung der Vererbung vorgeschlagen. Beispielverwendung der vorhandenen ForEach
-Hilfsmethode:
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
Option | Werte | Beschreibung |
---|---|---|
GeneratorMode | ( pgmClass , pgmUnit ) | Erzeugt nur den Klassenheader und die Implementierung oder eine ganze Einheit mit einer Klasse |
NameOfUnit | String | Name der generierten Einheit, der zum Erstellen des Einheitenkopfes verwendet wird |
NameOfClass | String | Name einer generierten Proxy-Klasse |
FieldNamingStyle | ( fnsUpperCaseF , fnsLowerCaseF ) | Legt fest, wie Klassenfelder benannt werden: mit dem Suffix F in Großbuchstaben oder mit Kleinbuchstaben |
IndentationText | String | Der Text verwendet für jede Codeeinrückung einen Standardwert von zwei Leerzeichen |
DataSetAccess | ( dsaNoAccess , dsaGenComment , dsaFullAccess ) | Definiert den Zugriff auf den internen Proxy-Datensatz: Vollzugriff = schreibgeschützte Eigenschaft wird generiert, um Zugriff zu haben. Die Option „Kein Zugriff“ ist die Standardeinstellung und wird empfohlen |
Um eine Poxy-Klasse zu generieren, können Sie die Execute-Methode verwenden, aber bevor Sie sie aufrufen, sollten Sie alle Optionen und DataSet
Eigenschaften einrichten. Nach dem Aufruf von Execute
wird der generierte Code in der internen TStringList
gespeichert, auf die über die Eigenschaft Code
zugegriffen werden kann. Siehe Beispielcode unten:
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 ;
Eine viel einfachere und kompaktere Möglichkeit zum Generieren von Proxy-Klassen ist die Verwendung von Generatorklassenmethoden: SaveToFile
oder SaveToClipboard
. Seine Namen sind aussagekräftig genug, um ihre Funktionalität zu verstehen. SaveToFile generiert die gesamte Einheit und schreibt sie in eine Datei, und SaveToClipboard generiert nur eine Klasse und schreibt in die Windows-Zwischenablage. Beispiele finden Sie unten:
TDataProxyGenerator.SaveToFile(
' src/Proxy.Employee ' ,
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
TDataProxyGenerator.SaveToClipboard(
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
Dieses Projekt ist das Ergebnis langjähriger Erfahrung und der Erfahrung mehrerer Teams. Dieses Team stellte fest, dass der klassische ereignisbasierte Delphi-Ansatz nicht nur weniger produktiv, sondern sogar gefährlich für die Entwickler, Manager und Kunden ist.
Die Arbeit mit RDBMS (SQL-Servern) in Delphi scheint sehr produktiv und einfach zu sein. Der Entwickler löscht eine Query
, gibt einen SQL-Befehl ein, legt die Active-Eigenschaft fest, verbindet alle DB-fähigen Steuerelemente mit der Abfrage und Sie sind fertig ... fast fertig, fast aber noch lange nicht bereit, die Anwendung bereitzustellen.
Mithilfe dieses einfachen visuellen Musters können Entwickler SQL Server-Daten extrem schnell offenlegen und ändern. Was auf den ersten Blick einfach aussieht, wird in Wirklichkeit zu einer Herausforderung. Mit der Zeit erstellen Ingenieure immer mehr Datensätze und Ereignisse, defragmentieren den Geschäftsablauf und vermischen Präsentations-, Konfigurations- und Domänencode. Das Projekt wird immer chaotischer und gekoppelter. Nach einigen Jahren verlieren Manager und Entwickler die Kontrolle über ein solches Projekt: Pläne und Fristen sind nicht quantifizierbar, Kunden kämpfen mit unerwarteten und seltsamen Fehlern, einfache Änderungen erfordern viele Arbeitsstunden.
Das Ersetzen des klassischen Datensatzes durch einen Proxy erfordert einige Zeit zum Erlernen und zur Validierung in der Praxis. Dieser Ansatz könnte für Delphi-Entwickler etwas seltsam aussehen, ist aber leicht zu übernehmen und zu erlernen. Mit der Motivation des Managements und dem leitenden Ingenieur wird das Coaching-Team die Code-Extraktion und das Ersetzen von Datensätzen durch Proxy-Technik schneller übernehmen.
Der hier definierte Proxy-Ansatz ist eine einfache und sichere Refactoring-Technik für klassische VCL-Anwendungen, die auf EDP-Methode (Event Driven Programming) erstellt wurden. Durch den evolutionären Einsatz dieser Lösung können kleine, aber wichtige Teile des Geschäftscodes extrahiert und mit Unit-Tests abgedeckt werden. Nach einiger Zeit können Ingenieure mit einem besseren Sicherheitsnetz (Abdeckung von Unit-Tests) Proxys mit OOP-DAOs austauschen und den Code durch erweiterte Refactorings und Architekturmuster weiter verbessern.
Der Modernisierungsprozess umfasst folgende Schritte:
Sehen Sie sich ein Beispiel an, das den Migrationspfad eines älteren VCL-Projekts mit einem TDataSetProxy zeigt. Wir beginnen mit der klassischen Methode, die in der Form definiert ist:
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 ;
Beachten! Die oben vorgestellte Lösung ist eine schlechte Praxis, wird aber leider häufig von Delphi-Entwicklern verwendet. Ziel des Einsatzes von TDataProxy ist es, diesen Zustand zu verbessern und die Geschäftslogik von der Visualisierung zu trennen.
Diese Methode lädt Daten aus einer SQL-Datenbank mithilfe von fdqBook
TFDQuery. Für jede Zeile wird ein Objekt der Klasse TBook
erstellt, dessen Felder mit Datensatzwerten gefüllt und validiert werden. Da TBook
Objekte im TListBox
Steuerelement gespeichert werden, das sie auch besitzt, muss diese Methode sie zuerst freigeben.
Wir ersetzen den Datensatz durch das Proxy-Objekt. Darüber hinaus modernisieren wir den Code, indem wir die klassische while-not-eof
-Schleife durch eine funktionale ForEach
-Methode ersetzen. Gleichzeitig führen wir eine sicherere Variante des Zugriffs auf Feldwerte ein. Es ist möglich, diese Phase in drei separate Phasen zu unterteilen, aber für diesen Artikel müssen wir den Inhalt kompakt halten.
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 ;
Der Code ist besser lesbar und sicherer, aber immer noch in der Form. Es ist an der Zeit, es zu entfernen und von allen Abhängigkeiten zu trennen, um Tests zu ermöglichen.
Wir müssen mit einer wichtigen architektonischen Entscheidung beginnen. Derzeit haben wir im Code zwei ähnliche Klassen: TBook
speichert Daten und TBookProxy
verarbeitet sie. Es ist wichtig zu entscheiden, welche dieser Klassen voneinander abhängt. TBook
ist Teil der Modellebene und sollte sich nicht über das Datenzugriffsobjekt im Klaren sein.
procedure TForm1.LoadBooksToListBox ();
begin
ListBox1.Clear;
fProxyBooks.LoadAndValidate;
fProxyBooks.FillStringsWithBooks(ListBox1.Items);
end ;
Schließlich sieht die Formularmethode schön und klar aus. Das ist ein gutes Zeichen dafür, dass wir in die richtige Richtung gehen. Der extrahierte und in einen Datensatz-Proxy verschobene Code sieht fast wie zuvor aus:
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 ;
Zusammen mit diesem Code mussten wir alle abhängigen Methoden verschieben, die für die Konvertierung und Validierung von Daten verantwortlich sind: BuildAuhtorsList
, ConvertReleaseDate
und ValidateCurrency
.
Dieser Proxy enthält eine interne Buchsammlung fBookList
, die zum Füllen der ListBox verwendet wird. Zu diesem Zeitpunkt haben wir diesen Code in die Datensatz-Proxy-Klasse verschoben, um die Anzahl der Änderungen zu reduzieren, aber er sollte in die richtige Klasse verschoben werden:
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
-Einheit)function CreateMockTableBook
in ( Data.Mock.Book.pas
-Einheit)