Alô, leitores!
Em continuidade aos artigos sobre Design Patterns, estudaremos o Builder, um padrão de projeto da mesma categoria do Abstract Factory, chamada “criacionais” (ou “de criação”). Recebem este nome por serem padrões que envolvem a criação de objetos em diferentes contextos. Vamos conhecer a proposta deste padrão de projeto?
Â
Se pesquisarmos por “Builder Pattern” na internet, encontraremos a seguinte definição:
É um padrão de projeto de software que permite a separação da construção de um objeto complexo da sua representação, de forma que o mesmo processo de construção possa criar diferentes representações.
Ao procuramos mais detalhes, descobriremos ainda que existem quatro elementos neste padrão, chamados de Director, Builder, Concrete Builder e Product.
Â
André, o que significa tudo isso?
Bom, vocês sabem que um dos maiores objetivos do blog é descomplicar conceitos. Para isso, costumo utilizar analogias para facilitar o entendimento. Neste artigo, não será diferente. Vou apresentar-lhes um exemplo que já é relativamente clássico com o Builder para esclarecer o papel de cada elemento: uma empresa de fast-food.
Imagine que você esteja em um fast-food e que tenha pedido um lanche ao atendente. Este, por sua vez, repassa o pedido para a cozinha, onde o lanche é produzido. Quando pronto, o lanche é entregue ao atendente e, por fim, a você. Nesse pequeno cenário, observa-se:
- O atendente é apenas um intermediário entre o cliente e a cozinha;
- O atendente não conhece como o lanche é feito. Apenas repassa o pedido do cliente;
- O lanche é feito pelos funcionários da cozinha, enquanto o atendente apenas o espera pronto.
Traduzindo em termos técnicos, é possÃvel ligar estes pontos aos elementos do Builder:
- O atendente é o Director, que solicita a construção de um objeto (produto);
- O lanche é o Product, ou seja, o produto resultante da construção;
- O quadro de funcionários que fazem os lanches é o Builder (Interface);
- O funcionário que preparou o lanche pedido é o Concrete Builder (objeto).
Dessa forma, podemos simplificar a definição do Builder:
É um padrão de projeto que permite encapsular a lógica de construção de objetos, de modo que diferentes representações possam ser criadas a partir de uma mesma interface.
Â
Por quê encapsular?
O encapsulamento refere-se ao fato do Director (atendente) não conhecer como o produto é fabricado. Apenas aguarda o produto pronto para ser entregue ao cliente.
O que são representações?
Considere que cada representação é um tipo de lanche que o fast-food vende. 🙂
Â
Em palavras gerais, o Builder tecnicamente funciona da seguinte forma: o Director “solicita” um Product para o Concrete Builder. Este é responsável por “construir” o produto – no qual denominamos de “complexo”, pois pode ser constituÃdo de várias partes – e devolver para o Director que, como disse anteriormente, não participa do processo de “construção”.
Acredito que o conceito ficará ainda mais claro com um exemplo prático. Neste artigo, codificaremos a emissão de um Relatório de Fornecedores em HTML.
Por quê?
Sabemos que a maioria dos relatórios são divididos em diferentes seções para facilitar a interpretação, certo? O objeto que representa um relatório, portanto, pode ser considerado como “complexo”, pois consiste de várias partes que podem ser condicionadas por regras de negócio. Neste exemplo, simularemos essa complexidade por meio de três passos:
- Construção do cabeçalho
- Construção do corpo
- Construção do rodapé
No entanto, este relatório não será um qualquer. Utilizaremos Bootstrap para deixá-lo com um visual mais agradável e responsivo.
Em primeiro lugar, definiremos a classe Product, que consiste no produto que será “construÃdo”. No nosso caso, será uma página HTML:
type
{ Product }
TProduct = class
private
// Objeto que armazenará o conteúdo HTML
FConteudoHTML: TStringList;
public
constructor Create;
destructor Destroy; override;
// Concatena o conteúdo HTML ao objeto "FConteudoHTML"
procedure Adicionar(const Texto: string);
// Abre uma caixa de diálogo para salvar o arquivo HTML
procedure SalvarArquivo;
end;
implementation
uses
System.SysUtils, Vcl.Dialogs;
{ TProduct }
procedure TProduct.Adicionar(const Texto: string);
begin
FConteudoHTML.Add(Texto);
end;
constructor TProduct.Create;
begin
FConteudoHTML := TStringList.Create;
end;
destructor TProduct.Destroy;
begin
FreeAndNil(FConteudoHTML);
inherited;
end;
procedure TProduct.SalvarArquivo;
var
SaveDialog: TSaveDialog;
begin
SaveDialog := TSaveDialog.Create(nil);
try
SaveDialog.Execute;
FConteudoHTML.SaveToFile(SaveDialog.FileName);
finally
FreeAndNil(SaveDialog);
end;
end;
O método “Adicionar” será o mais utilizado dessa classe, já que tem a função de concatenar as tags HTML para montagem da página.
Em segundo lugar, codificaremos a Interface Builder, que declara a sequência de passos para construir o produto:
uses
Pattern.Product;
type
{ Builder }
IBuilder = interface
// Métodos para criar as "partes" do objeto complexo
procedure BuildCabecalho;
procedure BuildDetalhes;
procedure BuildRodape;
// Função que retorna o "produto" pronto
function GetRelatorio: TProduct;
end;
Em seguida, precisamos de uma implementação concreta da Interface acima. Tal classe, que recebe o nome de Concrete Builder, será responsável por implementar cada passo da construção. No nosso exemplo, preencheremos as tags HTML referente à cada seção da página, fazendo referência aos templates do Bootstrap. Embora o código pareça um pouco extenso, a maior parte resume-se a comandos HTML.
uses
Pattern.Builder, Pattern.Product;
type
{ Concrete Builder }
TConcreteBuilder = class(TInterfacedObject, IBuilder)
private
FProduct: TProduct;
FDados: olevariant;
FQtdeRegistros: integer;
public
constructor Create(Dados: olevariant);
destructor Destroy; override;
// Métodos para criar as "partes" do objeto complexo
procedure BuildCabecalho;
procedure BuildDetalhes;
procedure BuildRodape;
// Função que retorna o "produto" pronto
function GetRelatorio: TProduct;
end;
implementation
uses
System.SysUtils, DataSnap.DBClient;
{ TConcreteBuilder }
procedure TConcreteBuilder.BuildCabecalho;
begin
// Preenche o conteúdo HTML referente ao cabeçalho
FProduct.Adicionar('<html><head><meta charset="UTF-8">');
// Importação das bibliotecas do Bootstrap
FProduct.Adicionar('<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">');
FProduct.Adicionar('<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>');
FProduct.Adicionar('</head>');
FProduct.Adicionar('<div class="container">');
FProduct.Adicionar('<div class="panel panel-default">');
FProduct.Adicionar('<div class="panel-heading">');
FProduct.Adicionar('<h4>Relatório de Fornecedores</h4>');
FProduct.Adicionar('<h4>' + FormatDateTime('dd/mm/yyyy hh:nn:ss', Now) + '</h4>');
FProduct.Adicionar('</div>');
end;
procedure TConcreteBuilder.BuildDetalhes;
var
DataSet: TClientDataSet;
begin
// Preenche o conteúdo HTML referente à tabela de registros
FProduct.Adicionar('<table class="table table-fixed">');
FProduct.Adicionar('<thead><tr>');
FProduct.Adicionar('<th class="col-xs-2">Código</th>');
FProduct.Adicionar('<th class="col-xs-8">Fornecedor</th>');
FProduct.Adicionar('<th class="col-xs-2">Cidade</th>');
FProduct.Adicionar('</tr></thead><tbody>');
DataSet := TClientDataSet.Create(nil);
try
DataSet.Data := FDados;
DataSet.First;
// Para cada iteração nos registros do DataSet, criauma linha no HTML
while not DataSet.Eof do
begin
FProduct.Adicionar('<tr>');
FProduct.Adicionar('<td class="col-xs-2">' + DataSet.FieldByName('VendorNo').AsString + '</td>');
FProduct.Adicionar('<td class="col-xs-2">' + DataSet.FieldByName('VendorName').AsString + '</td>');
FProduct.Adicionar('<td class="col-xs-2">' + DataSet.FieldByName('City').AsString + '</td>');
FProduct.Adicionar('</tr>');
// Incrementa a variável de quantidade de registros
Inc(FQtdeRegistros);
DataSet.Next;
end;
// Fecha o HTML da tabela
FProduct.Adicionar('</tbody></table></div></div>');
finally
FreeAndNil(DataSet);
end;
end;
procedure TConcreteBuilder.BuildRodape;
begin
// Preenche o conteúdo HTML referente ao rodapé
FProduct.Adicionar('<div id="footer">');
FProduct.Adicionar('<div class="container">');
FProduct.Adicionar('<p class="text-center">Qtde de Registros: ' + IntToStr(FQtdeRegistros)+ '</p>');
FProduct.Adicionar('</div></div></body></html>');
end;
constructor TConcreteBuilder.Create(Dados: olevariant);
begin
FQtdeRegistros := 0;
// Recebe os dados do formulário como parâmetro
FDados := Dados;
// Cria a instância do Product
FProduct := TProduct.Create;
end;
destructor TConcreteBuilder.Destroy;
begin
// Libera a instância do Product
FreeAndNil(FProduct);
inherited;
end;
function TConcreteBuilder.GetRelatorio: TProduct;
begin
// Devolve o produto pronto que, neste caso, é uma página HTML
result := FProduct;
end;
Não tem segredo, não é?
O último elemento, chamado Director, tem apenas a função de receber um Concrete Builder como parâmetro para executar a sequência de passos:
type
{ Director }
TDirector = class
public
// Método responsável por construir o objeto complexo por partes
procedure Construct(Builder: IBuilder);
end;
implementation
{ TDirector }
procedure TDirector.Construct(Builder: IBuilder);
begin
// Sequência de passos para construir o objeto complexo
Builder.BuildCabecalho;
Builder.BuildDetalhes;
Builder.BuildRodape;
end;
Recapitulando então, pessoal: tudo se inicia pelo Director, que equivale ao atendente na analogia no começo deste artigo. O Director recebe um Concrete Builder como parâmetro para executar a sequência de passos. No final deste processo, recebemos um Product “construÃdo”.
Para testar o padrão de projeto, elaborei um formulário simples, no qual carrega um arquivo de dados chamado “vendors.xml” que acompanha a instalação do RAD Studio.
No botão de geração do relatório, atente-se à codificação:
uses
Pattern.Director, Pattern.Builder, Pattern.ConcreteBuilder, Pattern.Product;
{...}
var
Director: TDirector;
ConcreteBuilder: IBuilder;
Product: TProduct;
begin
// Cria uma instância do Director
Director := TDirector.Create;
// Cria uma instância do Concrete Builder, informando os dados como parâmetro
ConcreteBuilder := TConcreteBuilder.Create(ClientDataSet.Data);
try
// Solicita a construção do objeto (relatório) ao Director
Director.Construct(ConcreteBuilder);
// Recebe o produto pronto (constrúido)
Product := ConcreteBuilder.GetRelatorio;
// Chama o método para salvar o arquivo em disco
Product.SalvarArquivo;
finally
// Libera o Director da memória
FreeAndNil(Director);
end;
end;
A tÃtulo de conhecimento, vale destacar que o Concrete Builder não precisa ser liberado da memória, já que foi declarado como Interface e o próprio Delphi se encarrega da liberação por meio de um mecanismo de contagem de referência.
Pois bem, este é o resultado:
Ou melhor, clique no link abaixo para acessar a página gerada:
Exemplo de Relatório em HTML gerado com o Design Pattern Builder
Ainda fiquei com uma dúvida: por que existe a Interface Builder?
O propósito do padrão de projeto Builder não limita-se a apenas construir objetos complexos. A sua proposta, além disso, é permitir que diferentes representações (ou seja, diferentes produtos) possam ser construÃdos com a mesma sequência de passos.
Considere, por exemplo, que seja necessário gerar um relatório em PDF com estes mesmos dados. TerÃamos apenas que criar um novo Product e um novo Concrete Builder encarregado de construir o cabeçalho, corpo e rodapé neste formato, provavelmente utilizando o QuickReport, FastReport ou Rave Reports. No formulário, basta indicar o Concrete Builder desejado como parâmetro do Director. O resto ocorre naturalmente. =D
Não compreendeu muito bem? Sem problemas! Clique no link a seguir para baixar o exemplo completo deste artigo:
Ou no GitHub:
https://github.com/AndreLuisCelestino/Delphi-DesignPatterns/tree/master/Builder-AndreCelestino
Grande abraço, pessoal!
Oi André,
muito legal ler seu conteúdo sobre Delphi, parabéns!
Mantenho um projeto chamado /dev/All, que é um agregador de blogs de desenvolvedores voltado para a comunidade. Nosso endereço é http://www.devall.com.br
AdorarÃamos ver seu conteúdo listado entre os posts do nosso agregador. Basta clicar em “Cadastre seu blog”.
Mais uma vez, parabéns pelo conteúdo!
Boa noite, Henrique!
Agradeço fortemente pela visita e também pelo convite para cadastrar o blog no agregador!
Vou fazer o cadastro o mais breve possÃvel. Será um grande prazer!
Parabéns pela iniciativa, Henrique. Um agregador como esse é muito importante para a comunidade de programadores.
Um grande abraço! 🙂
Fala André,
Para você ver, 2021 e o conteúdo ainda assim é altamente relevante. Parabéns e obrigado por contribuir de maneira riquÃssima. Trabalho com Delphi desde 2010 e não conhecia teu blog. André nesse caso especÃfico, sem se utilizar de outro padrão, adicionando outro Product e Concrete Builder, como citado, para indicar o desejado, utilizaria case ou if’s?
Olá, Claudiano, bom dia!
Primeiramente, obrigado pelo feedback!
Eu acredito que o conceito de Design Patterns jamais ficará obsoleto. Essas boas práticas sempre serão fundamentais para os desenvolvedores.
Claudiano, respondendo sua pergunta: sim, sem empregar outro padrão, seria necessário usar um if para instanciar o Builder desejado. Mesmo assim, eu aconselharia utilizar o Factory Method para atuar como uma “fábrica de Builders“.
Abração!