TDataSetProxy é um componente wrapper para o componente clássico do conjunto de dados Delphi. Permite substituir qualquer conjunto de dados por um conjunto de dados falso (tabela na memória). O proxy pode ser usado para separar uma classe de negócios de conjuntos de dados. Essa separação é útil quando o código de negócios precisa ser colocado em um conjunto de testes automatizados (testes de unidade).
Inspiração . A ideia é baseada no padrão Proxy GoF e no padrão Active Record, definidos por Martin Fowler no livro Patterns of Enterprise Application Architecture
O padrão DataSet Proxy é útil durante a extração da lógica de negócios. Isto poderia ser especialmente útil para melhorar projetos herdados e altamente acoplados. Quando o código de produção depende de dados SQL e de uma conexão SQL, é realmente difícil escrever testes de unidade para esse código.
A substituição do conjunto de dados por proxies introduz um novo nível de abstração que pode facilitar ambos: conjuntos de dados SQL no código de produção e conjuntos de dados de memória no projeto de teste. O Proxy possui interface (lista de métodos) muito semelhante ao conjunto de dados clássico, o que auxilia na fácil migração. Conjuntos de dados falsos permitirão verificar (afirmar) o código de produção sem conectar-se ao banco de dados.
O DataSet Proxy junto com dois projetos complementares (DataSet Generator, Delphi Command Pattern) oferece aos desenvolvedores a oportunidade de introduzir testes de unidade com refatorações seguras.
O proxy do conjunto de dados é uma solução temporária e depois de cobrir o código com os testes, os engenheiros podem aplicar refatorações mais avançadas: desacoplar o código ou torná-lo mais combinável e reutilizável. Como uma dessas refatorações, o proxy pode ser substituído com segurança pelo objeto DAO ou pelas estruturas de dados do modelo.
Juntamente com o código e a melhoria da qualidade, os desenvolvedores aprenderão como escrever um código mais limpo ou como usar a abordagem de teste primeiro e trabalhar melhor.
Projeto de apoio:
Projeto | Repositório GitHub |
---|---|
Gerador de conjunto de dados | https://github.com/bogdanpolak/dataset-generator |
O projeto inclui código fonte da classe base TDataSetProxy
e dois tipos diferentes de geradores de proxy:
src/Comp.Generator.DataProxy.pas
TDataSetProxy
tools/generator-app
O componente TDataProxyGenerator
é útil quando o engenheiro deseja gerar proxy para sair do conjunto de dados no código de produção. Esta é uma tarefa fácil de duas etapas: (1) adicionar unidade de componente à seção de uso, (2) encontrar o código usando o conjunto de dados e o método gerador de chamada:
Código de produção atual:
aBooksDataSet := fDBConnection.ConstructSQLDataSet(
aOwner, APPSQL_SelectBooks);
dbgridBooks.DataSource.Dataset := aBooksDataSet;
Código do gerador injetado:
TDataProxyGenerator.SaveToFile( ' ../../src/Proxy.Books ' ,
aBooksDataSet, ' TBookProxy ' );
Generator App for FireDAC é uma ferramenta alternativa criada principalmente para fins de demonstração. Na prática, usar esta ferramenta pode ser menos útil do que usar diretamente o gerador de componentes. Generator App é dedicado para fins de coaching e treinamento. Para mais informações consulte: App Generator para FireDAC - Guia do Usuário.
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 ;
O componente DataSetProxy é uma classe proxy, que possui métodos quase idênticos ao componente clássico TDataSet. O desenvolvedor pode facilmente substituir qualquer componente DataSet por este proxy aplicando apenas poucas alterações de baixo risco ao código de produção. Do ponto de vista do código de produção, a mudança é pequena e não muito importante, mas do ponto de vista do teste, esta é uma mudança fundamental, porque o desenvolvedor é capaz de reconfigurar o proxy para usar um conjunto de dados de memória leve.
A maioria dos métodos TDataSetProxy
são apenas clones de TDataSet uma vez. Você pode facilmente expandir o conjunto desses métodos adicionando os ausentes uma vez ou construir novos métodos exclusivos. Esses métodos de proxy são: Append
, Edit
, Cancel
, Delete
, Close
, Post
, RecordCount
, First
, Last
, Eof
, Next
, Prior
, EnableControls
, DisableControls
, Locate
, Lookup
, Refresh
e outros. A documentação e o uso deste método são iguais à documentação padrão do Delphi para a classe TDataSet
.
O restante dos métodos TDataSetProxy
podem ser divididos em dois grupos: métodos de configuração de proxy (configuração) e métodos auxiliares de proxy (expandindo a funcionalidade clássica do conjunto de dados).
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 ;
A versão atual do componente TDataSetProxy
contém apenas um método auxiliar que foi implementado como exemplo. Os desenvolvedores podem expandir esta coleção de acordo com as práticas de codificação da equipe. A sugestão de expandir a classe proxy é usar a herança. Exemplo de uso do 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
Opção | Valores | Descrição |
---|---|---|
GeneratorMode | ( pgmClass , pgmUnit ) | Gera apenas cabeçalho de classe e implementação ou unidade inteira com uma classe |
NameOfUnit | String | Nome da unidade gerada usada para criar o cabeçalho da unidade |
NameOfClass | String | Nome de uma classe de proxy gerada |
FieldNamingStyle | ( fnsUpperCaseF , fnsLowerCaseF ) | Decide como os campos de classe são nomeados: usando o sufixo F maiúsculo ou minúsculo |
IndentationText | String | O texto usa para cada recuo de código, o valor padrão é dois espaços |
DataSetAccess | ( dsaNoAccess , dsaGenComment , dsaFullAccess ) | Define o acesso ao conjunto de dados do proxy interno: acesso total = propriedade somente leitura é gerada para ter acesso. Nenhuma opção de acesso é padrão e recomendada |
Para gerar a classe poxy você pode usar o método Execute, mas antes de chamá-lo você deve configurar todas as opções e propriedades DataSet
. Após a chamada de Execute
o código gerado será armazenado no TStringList
interno acessível através da propriedade Code
. Veja o código de exemplo abaixo:
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 ;
Uma maneira muito mais fácil e compacta de gerar classes proxy é usar métodos de classe geradora: SaveToFile
ou SaveToClipboard
. Seus nomes são significativos o suficiente para compreender sua funcionalidade. SaveToFile gera a unidade inteira e a grava no arquivo e SaveToClipboard gera apenas uma classe e grava na área de transferência do Windows. Veja exemplos abaixo:
TDataProxyGenerator.SaveToFile(
' src/Proxy.Employee ' ,
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
TDataProxyGenerator.SaveToClipboard(
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
Este projeto é fruto de muitos anos e da experiência de múltiplas equipes. Essas equipes descobriram que a abordagem clássica do Delphi baseada em eventos não é apenas menos produtiva, mas até perigosa para os desenvolvedores, os gerentes e os clientes.
Trabalhar com RDBMS (servidores SQL) em Delphi parece ser muito produtivo e simples. O desenvolvedor descarta um componente Query
, insere o comando SQL, define a propriedade Active, conecta todos os controles com reconhecimento de banco de dados à consulta e você está pronto... quase pronto, quase, mas na verdade longe de estar pronto para entregar o aplicativo.
Usar esse desenvolvedor de padrão visual simples pode expor e modificar dados do servidor SQL com extrema rapidez. Na realidade, o que à primeira vista parece simples, depois se torna um desafio. Com o tempo, os engenheiros criam cada vez mais conjuntos de dados e eventos, desfragmentando o fluxo de negócios e misturando apresentação, configuração e código de domínio. O projeto se torna cada vez mais confuso e acoplado. Depois de alguns anos, gerentes e desenvolvedores perdem o controle sobre esse projeto: planos e prazos não são possíveis de quantificar, os clientes enfrentam bugs inesperados e estranhos, mudanças simples exigem muitas horas de trabalho.
Substituir o conjunto de dados clássico por proxy requer algum tempo para aprender e validar em ação. Essa abordagem pode parecer um pouco estranha para desenvolvedores Delphi, mas é fácil de adotar e aprender. Com motivação de gerenciamento e equipe de coaching de engenheiros seniores, adotará mais rapidamente a extração de código e a substituição de conjuntos de dados por técnicas de proxies.
Definida aqui, a abordagem proxy é uma técnica de refatoração simples e segura dedicada para aplicativos VCL clássicos construídos no modo EDP (Event Driven Programming). Usando esta solução em evolução, partes pequenas, mas importantes do código de negócios podem ser extraídas e cobertas com testes de unidade. Depois de algum tempo, com uma rede de segurança melhor (cobertura de testes unitários), os engenheiros podem trocar proxies com DAOs OOP e melhorar ainda mais o código usando refatorações avançadas e padrões de arquitetura.
O processo de modernização inclui as seguintes etapas:
Veja um exemplo que mostra o caminho de migração de um projeto VCL legado usando um TDataSetProxy. Começaremos com o método clássico definido no formato:
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 ;
Perceber! A solução apresentada acima é uma prática ruim, mas infelizmente é frequentemente usada por desenvolvedores Delphi. O objetivo do uso do TDataProxy é melhorar esse estado e separar a lógica de negócios da visualização.
Este método carrega dados do banco de dados SQL, usando fdqBook
TFDQuery. Para cada linha é criado um objeto da classe TBook
, seus campos são preenchidos com os valores do conjunto de dados e validados. Como os objetos TBook
são armazenados no controle TListBox
, que também os possui, este método deve liberá-los primeiro.
Substituímos o conjunto de dados pelo objeto proxy. Além disso, estamos modernizando o código alterando o clássico loop while-not-eof
por um método ForEach
funcional. Ao mesmo tempo, estamos introduzindo uma variante mais segura de acesso aos valores dos campos. É possível separar esta fase em 3 fases distintas, mas para este artigo precisamos manter o conteúdo 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 ;
O código é mais legível e seguro, mas ainda está no formato. É hora de removê-lo e separá-lo de todas as dependências para permitir os testes.
Devemos começar com uma importante decisão arquitetônica. Atualmente no código temos duas classes semelhantes: TBook
armazenando dados e TBookProxy
processando-os. É importante decidir qual dessas classes depende da outra. TBook
faz parte da camada de modelo e não deve estar ciente do objeto de acesso a dados.
procedure TForm1.LoadBooksToListBox ();
begin
ListBox1.Clear;
fProxyBooks.LoadAndValidate;
fProxyBooks.FillStringsWithBooks(ListBox1.Items);
end ;
Finalmente, o método do formulário parece bonito e claro. Este é um bom sinal de que estamos indo na direção certa. O código extraído e movido para um proxy de conjunto de dados se parece quase com o 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 com este código tivemos que mover todos os métodos dependentes responsáveis pela conversão e validação dos dados: BuildAuhtorsList
, ConvertReleaseDate
e ValidateCurrency
.
Este proxy contém uma coleção interna de livros fBookList
que é usada para preencher ListBox. Naquele momento, movemos este código para a classe proxy do conjunto de dados para reduzir o número de alterações, mas a carta deve ser movida para a classe adequada:
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
em (unidade Data.Proxy.Book.pas
)function CreateMockTableBook
em (unidade Data.Mock.Book.pas
)