TDataSetProxy est un composant wrapper pour le composant d'ensemble de données Delphi classique. Il permet de remplacer n'importe quel ensemble de données par un faux ensemble de données (table en mémoire). Le proxy peut être utilisé pour séparer une classe métier des ensembles de données. Cette séparation est utile lorsque le code métier doit être placé dans un faisceau de tests automatisés (tests unitaires).
Inspiration . L'idée est basée sur le modèle Proxy GoF et le modèle Active Record, définis par Martin Fowler dans le livre Patterns of Enterprise Application Architecture.
Le modèle DataSet Proxy est utile lors de l’extraction de la logique métier. Cela pourrait être particulièrement utile pour améliorer les projets existants hautement couplés. Lorsque le code de production dépend de données SQL et d'une connexion SQL, il est très difficile d'écrire des tests unitaires pour un tel code.
Le remplacement des ensembles de données par des proxys introduit un nouveau niveau d'abstraction qui peut faciliter à la fois : les ensembles de données SQL dans le code de production et les ensembles de données mémoire dans le projet de test. Le proxy a une interface très similaire (liste de méthodes) à l'ensemble de données classique, ce qui facilite la migration. De faux ensembles de données permettront de vérifier (affirmer) le code de production sans se connecter à la base de données.
DataSet Proxy ainsi que deux projets compagnons (DataSet Generator, Delphi Command Pattern) donnent aux développeurs la possibilité d'introduire des tests unitaires avec des refactorisations sécurisées.
Le proxy d'ensemble de données est une solution temporaire et après avoir couvert le code avec les tests, les ingénieurs peuvent appliquer des refactorisations plus avancées : découplage du code ou le rendre plus composable et réutilisable. Comme l'une de ces refactorisations, le proxy peut être remplacé en toute sécurité par l'objet DAO ou par les structures de données du modèle.
En collaboration avec le code et l'amélioration de la qualité, les développeurs apprendront à écrire du code plus propre ou à utiliser l'approche test d'abord et à mieux travailler.
Projet solidaire :
Projet | Dépôt GitHub |
---|---|
Générateur de jeux de données | https://github.com/bogdanpolak/dataset-generator |
Le projet comprend le code source de la classe de base TDataSetProxy
et deux types différents de générateurs de proxy :
src/Comp.Generator.DataProxy.pas
TDataSetProxy
tools/generator-app
Le composant TDataProxyGenerator
est utile lorsque l'ingénieur souhaite générer un proxy pour quitter l'ensemble de données dans le code de production. Il s'agit d'une tâche simple en deux étapes : (1) ajouter une unité de composant à la section des utilisations, (2) rechercher du code à l'aide de l'ensemble de données et de la méthode du générateur d'appel :
Code de production actuel :
aBooksDataSet := fDBConnection.ConstructSQLDataSet(
aOwner, APPSQL_SelectBooks);
dbgridBooks.DataSource.Dataset := aBooksDataSet;
Code du générateur injecté :
TDataProxyGenerator.SaveToFile( ' ../../src/Proxy.Books ' ,
aBooksDataSet, ' TBookProxy ' );
Generator App for FireDAC est un outil alternatif créé principalement à des fins de démonstration. En pratique, utiliser cet outil peut être moins utile que d'utiliser directement le générateur de composants. L'application Generator est dédiée à des fins de coaching et de formation. Pour plus d'informations, consultez : Application Generator pour FireDAC - Guide de l'utilisateur.
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 ;
Le composant DataSetProxy est une classe proxy, qui a des méthodes presque identiques au composant TDataSet classique. Le développeur peut facilement remplacer n'importe quel composant DataSet par ce proxy en appliquant seulement quelques modifications et à faible risque au code de production. Du point de vue du code de production, le changement est petit et peu important, mais du point de vue des tests, il s'agit d'un changement fondamental, car le développeur est capable de reconfigurer le proxy pour utiliser un ensemble de données de mémoire léger.
La plupart des méthodes TDataSetProxy
ne sont que des clones de TDataSet. Vous pouvez facilement étendre l'ensemble de ces méthodes en ajoutant les méthodes manquantes une fois ou en créer de nouvelles uniques. Ces méthodes de proxy sont : Append
, Edit
, Cancel
, Delete
, Close
, Post
, RecordCount
, First
, Last
, Eof
, Next
, Prior
, EnableControls
, DisableControls
, Locate
, Lookup
, Refresh
et autres. La documentation et l'utilisation de ces méthodes sont les mêmes que la documentation Delphi standard pour la classe TDataSet
.
Le reste des méthodes TDataSetProxy
peuvent être divisées en deux groupes : les méthodes de configuration de proxy (configuration) et les méthodes d'assistance de proxy (extension de la fonctionnalité d'ensemble de données classique).
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 ;
La version actuelle du composant TDataSetProxy
ne contient qu'une seule méthode d'assistance qui a été implémentée à titre d'exemple. Les développeurs peuvent élargir cette collection en fonction des pratiques de codage de l'équipe. Il est suggéré d'étendre la classe proxy en utilisant l'héritage. Exemple d'utilisation de la méthode d'assistance ForEach
existante :
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 | Valeurs | Description |
---|---|---|
GeneratorMode | ( pgmClass , pgmUnit ) | Génère uniquement l'en-tête de classe et l'implémentation ou une unité entière avec une classe |
NameOfUnit | String | Nom de l'unité générée utilisée pour créer l'en-tête de l'unité |
NameOfClass | String | Nom d'une classe proxy générée |
FieldNamingStyle | ( fnsUpperCaseF , fnsLowerCaseF ) | Décide comment les champs de classe sont nommés : en utilisant le suffixe F majuscule ou en minuscules |
IndentationText | String | Le texte est utilisé pour chaque indentation de code, la valeur par défaut est de deux espaces |
DataSetAccess | ( dsaNoAccess , dsaGenComment , dsaFullAccess ) | Définit l'accès à l'ensemble de données proxy interne : accès complet = une propriété en lecture seule est générée pour avoir un accès. Aucune option d'accès n'est par défaut et est recommandée |
Pour générer une classe poxy, vous pouvez utiliser la méthode Execute, mais avant de l'appeler, vous devez configurer toutes les options et propriétés DataSet
. Après avoir appelé Execute
le code généré sera stocké dans la TStringList
interne accessible via la propriété Code
. Voir l'exemple de code ci-dessous :
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 ;
Un moyen beaucoup plus simple et compact de générer des classes proxy consiste à utiliser les méthodes de classe génératrice : SaveToFile
ou SaveToClipboard
. Ses noms sont suffisamment significatifs pour comprendre leur fonctionnalité. SaveToFile génère une unité entière et l'écrit dans un fichier et SaveToClipboard génère uniquement une classe et écrit dans le Presse-papiers Windows. Voir les exemples ci-dessous :
TDataProxyGenerator.SaveToFile(
' src/Proxy.Employee ' ,
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
TDataProxyGenerator.SaveToClipboard(
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
Ce projet est le résultat de nombreuses années et de l'expérience de plusieurs équipes. Ces équipes ont constaté que l'approche Delphi classique basée sur les événements est non seulement moins productive, mais même dangereuse pour les développeurs, les managers et les clients.
Travailler avec des SGBDR (serveurs SQL) dans Delphi semble très productif et simple. Le développeur supprime un composant Query
, entre la commande SQL, définit la propriété Active, connecte tous les contrôles compatibles DB à la requête et vous avez terminé... presque terminé, presque mais en réalité loin d'être prêt à fournir l'application.
L'utilisation de ce modèle visuel simple permet au développeur d'exposer et de modifier les données du serveur SQL extrêmement rapidement. En réalité, ce qui semble simple au moment de la mendicité devient un défi. Au fil du temps, les ingénieurs créent de plus en plus d'ensembles de données et d'événements, défragmentant le flux commercial et mélangeant présentation, configuration et code de domaine. Le projet devient de plus en plus brouillon et couplé. Après quelques années, les gestionnaires et les développeurs perdent le contrôle d'un tel projet : les plans et les délais ne sont pas quantifiables, les clients sont confrontés à des bugs inattendus et étranges, de simples changements nécessitent de nombreuses heures de travail.
Remplacer l'ensemble de données classique par un proxy nécessite un certain temps d'apprentissage et de validation en action. Cette approche peut paraître un peu étrange pour les développeurs Delphi, mais elle est facile à adopter et à apprendre. Avec la motivation de la direction et l'équipe de coaching d'ingénieurs senior, elle adoptera plus rapidement l'extraction de code et le remplacement des ensembles de données par une technique de proxy.
L'approche proxy définie ici est une technique de refactoring simple et sûre dédiée aux applications VCL classiques construites de manière EDP (Event Driven Programming). En utilisant cette solution en évolution, des parties petites mais importantes du code métier peuvent être extraites et couvertes par des tests unitaires. Après un certain temps, avec un meilleur filet de sécurité (couverture des tests unitaires), les ingénieurs peuvent échanger des proxys avec des DAO POO et améliorer davantage le code à l'aide de refactorisations avancées et de modèles architecturaux.
Le processus de modernisation comprend les étapes suivantes :
Regardez un exemple montrant le chemin de migration d'un projet VCL existant à l'aide d'un TDataSetProxy. Nous allons commencer par la méthode classique définie sous la forme :
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 ;
Avis! La solution présentée ci-dessus est une mauvaise pratique, mais elle est malheureusement souvent utilisée par les développeurs Delphi. L'objectif de l'utilisation de TDataProxy est d'améliorer cet état et de séparer la logique métier de la visualisation.
Cette méthode charge les données de la base de données SQL, à l'aide de fdqBook
TFDQuery. Un objet de classe TBook
est créé pour chaque ligne, ses champs sont remplis avec les valeurs du jeu de données et validés. Étant donné que les objets TBook
sont stockés dans le contrôle TListBox
, qui les possède également, cette méthode doit d'abord les libérer.
Nous remplaçons l'ensemble de données par l'objet proxy. De plus, nous modernisons le code en remplaçant la boucle classique while-not-eof
par une méthode ForEach
fonctionnelle. Dans le même temps, nous introduisons une variante plus sûre d’accès aux valeurs des champs. Il est possible de séparer cette phase en 3 phases distinctes, mais pour cet article, nous devons garder le contenu compact.
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 ;
Le code est plus lisible et plus sûr, mais reste sous forme. Il est temps de le supprimer et de le séparer de toutes les dépendances pour permettre les tests.
Il faut commencer par une décision architecturale importante. Actuellement dans le code, nous avons deux classes similaires : TBook
stockant les données et TBookProxy
les traitant. Il est important de décider laquelle de ces classes dépend de l’autre. TBook
fait partie de la couche modèle et ne doit pas connaître l'objet d'accès aux données.
procedure TForm1.LoadBooksToListBox ();
begin
ListBox1.Clear;
fProxyBooks.LoadAndValidate;
fProxyBooks.FillStringsWithBooks(ListBox1.Items);
end ;
Enfin, la méthode du formulaire semble claire et nette. C’est un bon signe que nous allons dans la bonne direction. Le code extrait et déplacé vers un proxy d'ensemble de données ressemble presque au précédent :
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 ;
Avec ce code, nous avons dû déplacer toutes les méthodes dépendantes responsables de la conversion et de la validation des données : BuildAuhtorsList
, ConvertReleaseDate
et ValidateCurrency
.
Ce proxy contient la collection interne du livre fBookList
qui est utilisée pour remplir ListBox. À ce moment-là, nous avons déplacé ce code vers la classe proxy de l'ensemble de données pour réduire le nombre de modifications, mais il doit être déplacé dans la classe appropriée :
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
dans (unité Data.Proxy.Book.pas
)function CreateMockTableBook
dans (unité Data.Mock.Book.pas
)