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?
Introdução
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.
Analogia
Bom, vocês sabem que um dos maiores objetivos do blog é descomplicar conceitos. Para isso, costumo utilizar analogias para facilitar o entendimento. Este 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”.
Exemplo de codificação do Builder
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.
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.
Classe Product
Em primeiro lugar, definiremos a classe Product, que consiste no produto que será “construído”. No nosso caso, será uma página HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
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.
Interface Builder
Em segundo lugar, codificaremos a Interface Builder, que declara a sequência de passos para construir o produto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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; |
Classe Concrete Builder
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
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 é?
Classe Director
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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; |
Em ação!
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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:
Conclusão
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, FastReports ou Rave Reports. No formulário, basta indicar o Concrete Builder desejado como parâmetro do Director. O resto ocorre naturalmente. 🙂
Não compreendeu muito bem? Sem problemas! Clique no link a seguir para baixar o exemplo completo deste artigo:
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!