TDataSetProxy es un componente contenedor para el componente de conjunto de datos clásico de Delphi. Permite reemplazar cualquier conjunto de datos con un conjunto de datos falso (tabla en memoria). El proxy se puede utilizar para separar una clase empresarial de los conjuntos de datos; esta separación es útil cuando el código empresarial debe incluirse en un conjunto de pruebas automatizadas (pruebas unitarias).
Inspiración . La idea se basa en el patrón Proxy GoF y el patrón Active Record, definidos por Martin Fowler en el libro Patterns of Enterprise Application Architecture.
El patrón DataSet Proxy es útil durante la extracción de la lógica empresarial. Esto podría resultar especialmente útil para mejorar proyectos heredados y altamente acoplados. Cuando el código de producción depende de datos SQL y de una conexión SQL, es realmente difícil escribir pruebas unitarias para dicho código.
Reemplazar el conjunto de datos con servidores proxy introduce un nuevo nivel de abstracción que puede facilitar ambos: conjuntos de datos SQL en el código de producción y conjuntos de datos de memoria en el proyecto de prueba. Proxy tiene una interfaz muy similar (lista de métodos) al conjunto de datos clásico, lo que ayuda a facilitar la migración. Los conjuntos de datos falsos permitirán verificar (afirmar) el código de producción sin conectarse a la base de datos.
DataSet Proxy junto con dos proyectos complementarios (DataSet Generator, Delphi Command Pattern) brinda a los desarrolladores la oportunidad de introducir pruebas unitarias con refactorizaciones seguras.
El proxy del conjunto de datos es una solución temporal y, después de cubrir el código con las pruebas, los ingenieros pueden aplicar refactorizaciones más avanzadas: desacoplar el código o hacerlo más componible y reutilizable. Como una de estas refactorizaciones, el proxy se puede reemplazar de forma segura por el objeto DAO o por las estructuras de datos del modelo.
Junto con el código y la mejora de la calidad, los desarrolladores aprenderán cómo escribir código más limpio o cómo utilizar el enfoque de prueba primero y trabajar mejor.
Proyecto de apoyo:
Proyecto | Repositorio de GitHub |
---|---|
Generador de conjuntos de datos | https://github.com/bogdanpolak/dataset-generator |
El proyecto incluye el código fuente de la clase base TDataSetProxy
y dos tipos diferentes de generadores de proxy:
src/Comp.Generator.DataProxy.pas
TDataSetProxy
tools/generator-app
El componente TDataProxyGenerator
es útil cuando el ingeniero desea generar un proxy para salir del conjunto de datos en el código de producción. Esta es una tarea sencilla de dos pasos: (1) agregar la unidad de componentes a la sección de usos, (2) buscar código usando el conjunto de datos y llamar al método generador:
Código de producción actual:
aBooksDataSet := fDBConnection.ConstructSQLDataSet(
aOwner, APPSQL_SelectBooks);
dbgridBooks.DataSource.Dataset := aBooksDataSet;
Código generador inyectado:
TDataProxyGenerator.SaveToFile( ' ../../src/Proxy.Books ' ,
aBooksDataSet, ' TBookProxy ' );
La aplicación Generator para FireDAC es una herramienta alternativa creada principalmente con fines de demostración. En la práctica, utilizar esta herramienta puede resultar menos útil que utilizar directamente el generador de componentes. La aplicación Generator está dedicada a fines de entrenamiento y entrenamiento. Para obtener más información, consulte: Aplicación Generator para FireDAC - Guía del usuario.
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 ;
El componente DataSetProxy es una clase de proxy, que tiene métodos casi idénticos al componente TDataSet clásico. El desarrollador puede reemplazar fácilmente cualquier componente de DataSet con este proxy aplicando solo unos pocos cambios y de bajo riesgo al código de producción. Desde el punto de vista del código de producción, el cambio es pequeño y no muy importante, pero desde la perspectiva de las pruebas, este es un cambio fundamental, porque el desarrollador puede reconfigurar el proxy para usar un conjunto de datos de memoria liviano.
La mayoría de los métodos TDataSetProxy
son solo clones de TDataSet una vez. Puede expandir fácilmente el conjunto de estos métodos agregando los que faltan una vez o crear otros nuevos y únicos. Estos métodos de proxy son: Append
, Edit
, Cancel
, Delete
, Close
, Post
, RecordCount
, First
, Last
, Eof
, Next
, Prior
, EnableControls
, DisableControls
, Locate
, Lookup
, Refresh
y otros. La documentación y el uso de estos métodos son los mismos que la documentación estándar de Delphi para la clase TDataSet
.
El resto de los métodos TDataSetProxy
se pueden dividir en dos grupos: métodos de configuración de proxy (configuración) y métodos auxiliares de proxy (que amplían la funcionalidad del conjunto de datos clásico).
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 versión actual del componente TDataSetProxy
contiene solo un método auxiliar que se implementó como ejemplo. Los desarrolladores pueden ampliar esta colección según las prácticas de codificación del equipo. Lo que se sugiere para expandir la clase de proxy es usar la herencia. Uso de muestra del método auxiliar ForEach
existente:
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
Opción | Valores | Descripción |
---|---|---|
GeneratorMode | ( pgmClass , pgmUnit ) | Genera solo encabezado de clase e implementación o unidad completa con una clase |
NameOfUnit | String | Nombre de la unidad generada que se utiliza para crear el encabezado de la unidad |
NameOfClass | String | Nombre de una clase de proxy generada |
FieldNamingStyle | ( fnsUpperCaseF , fnsLowerCaseF ) | Decide cómo se nombran los campos de clase: usando el sufijo F mayúscula o minúscula |
IndentationText | String | Usos de texto para cada sangría de código, el valor predeterminado es dos espacios |
DataSetAccess | ( dsaNoAccess , dsaGenComment , dsaFullAccess ) | Define el acceso al conjunto de datos del proxy interno: acceso completo = se genera una propiedad de solo lectura para tener acceso. Ninguna opción de acceso es predeterminada y recomendada |
Para generar una clase poxy, puede usar el método Execute, pero antes de llamarlo debe configurar todas las opciones y propiedades DataSet
. Después de llamar Execute
el código generado se almacenará en la TStringList
interna accesible a través de la propiedad Code
. Vea el código de muestra a continuación:
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 ;
Una forma mucho más sencilla y compacta de generar clases de proxy es utilizar métodos de clases generadoras: SaveToFile
o SaveToClipboard
. Sus nombres son lo suficientemente significativos como para comprender su funcionalidad. SaveToFile genera la unidad completa y la escribe en un archivo y SaveToClipboard genera solo una clase y escribe en el Portapapeles de Windows. Vea ejemplos a continuación:
TDataProxyGenerator.SaveToFile(
' src/Proxy.Employee ' ,
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
TDataProxyGenerator.SaveToClipboard(
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
Este proyecto es el resultado de muchos años y la experiencia de múltiples equipos. Estos equipos descubrieron que el enfoque clásico de Delphi basado en eventos no sólo es menos productivo, sino incluso peligroso para los desarrolladores, los gerentes y los clientes.
Trabajar con RDBMS (servidores SQL) en Delphi parece muy productivo y sencillo. El desarrollador suelta un componente Query
, ingresa un comando SQL, establece la propiedad Activa, conecta todos los controles compatibles con la base de datos para realizar la consulta y ya está... casi terminado, casi, pero en realidad lejos de estar listo para entregar la aplicación.
El uso de este sencillo desarrollador de patrones visuales puede exponer y modificar los datos del servidor SQL con extrema rapidez. En realidad, lo que parece simple al principio, luego se convierte en un desafío. Con el tiempo, los ingenieros crean cada vez más conjuntos de datos y eventos, desfragmentando el flujo de negocios y mezclando presentación, configuración y código de dominio. El proyecto se vuelve cada vez más desordenado y acoplado. Después de algunos años, los gerentes y desarrolladores pierden el control sobre dicho proyecto: los planes y los plazos no son posibles de cuantificar, los clientes luchan con errores inesperados y extraños, los cambios simples requieren muchas horas de trabajo.
Reemplazar el conjunto de datos clásico con un proxy requiere algo de tiempo para aprender y validar en acción. Este enfoque puede parecer un poco extraño para los desarrolladores de Delphi, pero es fácil de adoptar y aprender. Con la motivación de la gerencia y el equipo de capacitación de ingenieros senior, adoptará más rápidamente la extracción de código y reemplazará conjuntos de datos con técnicas de proxy.
El enfoque de proxy definido aquí es una técnica de refactorización simple y segura dedicada a aplicaciones VCL clásicas construidas en forma EDP (programación impulsada por eventos). Al utilizar esta solución en la evolución, se pueden extraer y cubrir partes del código empresarial pequeñas pero importantes con pruebas unitarias. Después de un tiempo, con una mejor red de seguridad (cobertura de pruebas unitarias), los ingenieros pueden intercambiar proxies con OOP DAO y mejorar más el código utilizando refactorizaciones avanzadas y patrones arquitectónicos.
El proceso de modernización incluye los siguientes pasos:
Mire el ejemplo que muestra la ruta de migración de un proyecto VCL heredado utilizando un TDataSetProxy. Comenzaremos con el método clásico definido en el formulario:
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 ;
¡Aviso! La solución presentada anteriormente es una mala práctica, pero desafortunadamente los desarrolladores de Delphi la utilizan a menudo. El objetivo de utilizar TDataProxy es mejorar este estado y separar la lógica empresarial de la visualización.
Este método consiste en cargar datos desde una base de datos SQL, utilizando fdqBook
TFDQuery. Se crea un objeto de clase TBook
para cada fila, sus campos se completan con valores del conjunto de datos y se validan. Debido a que los objetos TBook
se almacenan en el control TListBox
, que también los posee, este método debe liberarlos primero.
Reemplazamos el conjunto de datos con el objeto proxy. Además, estamos modernizando el código cambiando el bucle clásico while-not-eof
por un método ForEach
funcional. Al mismo tiempo, estamos introduciendo una variante más segura para acceder a los valores de los campos. Es posible separar esta fase en 3 fases separadas, pero para este artículo necesitamos mantener el contenido compacto.
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 ;
El código es más legible y seguro, pero todavía está en el formulario. Es hora de eliminarlo y separarlo de todas las dependencias para permitir las pruebas.
Debemos comenzar con una decisión arquitectónica importante. Actualmente en el código tenemos dos clases similares: TBook
que almacena datos y TBookProxy
que los procesa. Es importante decidir cuál de estas clases depende de la otra. TBook
es parte de la capa del modelo y no debe tener conocimiento del objeto de acceso a datos.
procedure TForm1.LoadBooksToListBox ();
begin
ListBox1.Clear;
fProxyBooks.LoadAndValidate;
fProxyBooks.FillStringsWithBooks(ListBox1.Items);
end ;
Finalmente, el método del formulario se ve bonito y claro. Ésta es una buena señal de que vamos en la dirección correcta. El código extraído y movido a un proxy de conjunto de datos se ve casi como el anterior:
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 ;
Junto con este código, tuvimos que mover todos los métodos dependientes responsables de convertir y validar datos: BuildAuhtorsList
, ConvertReleaseDate
y ValidateCurrency
.
Este proxy contiene una colección interna de libros fBookList
que se utiliza para llenar ListBox. En ese momento movimos este código a la clase de proxy del conjunto de datos para reducir la cantidad de cambios, pero debería trasladarse a la clase adecuada:
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
en (unidad Data.Proxy.Book.pas
)function CreateMockTableBook
en (unidad Data.Mock.Book.pas
)