Retomando a nossa série de artigos sobre Design Patterns, hoje apresento-lhes o padrão Bridge. Pela tradução – “ponte” – já podemos imaginar um pouco do propósito deste padrão, concordam? Talvez seja uma classe que “conecte” duas partes do sistema, como uma ponte real liga duas cidades.
Bom, não é bem isso. Acompanhe o artigo e conheça a ideia por trás deste padrão!
Introdução
Embora a tradução de “bridge” seja “ponte”, veremos que o objetivo do padrão de projeto Bridge não é conectar duas abstrações diferentes. Essa é uma solução do Adapter, discutido no artigo anterior, que atua como um intermediário entre duas classes para torná-las compatÃveis. De forma bastante resumida, o propósito do Bridge é eliminar múltiplas heranças e reduzir a quantidade de classes existentes no projeto.
Cenário
Para iniciar, vou apresentar um cenário bem tÃpico. Considere que a nossa aplicação possua uma funcionalidade de exportação de dados de clientes e produtos para os formatos XLS e HTML. Para evitar a duplicação de código, há uma classe base chamada TExportador
e duas heranças a partir dela: TExportadorClientes
e TExportadorProdutos
. Como exportamos para dois formatos diferentes, precisamos, agora, criar uma especialização para cada uma dessas classes filhas:
TExportadorClientesXLS
TExportadorClientesHTML
TExportadorProdutosXLS
TExportadorProdutosHTML
Até o momento, essa é a nossa hierarquia de classes:
Embora pareça viável, essa hierarquia está sujeita a se transformar em um emaranhado de classes. Sabe por quê?
Imagine que o cliente tenha solicitado a funcionalidade de exportação dos dados de fornecedores também. E mais, além de XLS e HTML, o cliente precisará de exportação em formato CSV para importar o arquivo em outro sistema. Se seguirmos a lógica da hierarquia apresentada, esta será a nova arquitetura:
6 classes novas?
Pois é. A imagem quase nem coube na página. Imagine então se surgisse a necessidade de exportação dos dados de funcionários? No mÃnimo seriam mais 4 classes criadas. Aos poucos, a arquitetura torna-se uma “macarronada” de classes, dificultando não só a manutenção, como também a evolução dessas funcionalidades. Veja que o conceito de Herança da Orientação a Objetos, apesar de muito importante, pode ser um complicador quando empregado abusivamente.
Bridge
Essa situação pode ser resolvida com o padrão de projeto Bridge!
Na teoria, o padrão de projeto traz a seguinte descrição: “Desacoplar uma abstração de sua implementação”. Traduzindo na prática, o que faremos a seguir é separar e agrupar as responsabilidades em diferentes classes, reduzindo radicalmente a complexidade da arquitetura. Para este propósito, o padrão Bridge é composto por 4 elementos:
- Abstraction: classe abstrata ou interface da abstração principal;
- Refined Abstraction: implementação concreta da Abstraction;
- Implementor: interface da abstração utilizada pela Abstraction;
- Concrete Implementor: implementação concreta da Implementor.
Você deve ter levantado a sobrancelha ao ler a função de cada um dos elementos, não é? 🙂
Compreendo que podem parecem semelhantes, ou talvez confusos, mas o exemplo prático a seguir será mais elucidativo.
Corrigindo a arquitetura com o Bridge
Para “consertar” a nossa arquitetura com o padrão Bridge, devemos, inicialmente, separar o escopo das exportações e os tipos de formatos, resultando em duas classes base, ao invés de uma só. O objeto da classe de exportação receberá um objeto da classe de tipo de formato para realizar a exportação. Em outras palavras, “injetaremos” o tipo de formato no objeto de exportação. Feito isso, a nossa arquitetura ficará dessa forma:
Bem melhor, não?
Interface Implementor
Então, mãos à obra! Em primeiro lugar, criaremos a Interface do Implementor, que se refere ao tipo de formato:
1 2 3 4 5 6 7 8 |
type { Implementor } IFormato = interface // métodos padrão para manipular a exportação procedure PularLinha; procedure ExportarCampo(const Valor: string); procedure SalvarArquivo(const NomeArquivo: string); end; |
Classes Concrete Implementors
Em seguida, criaremos uma classe para cada tipo de formato, implementando a Interface acima. Essas classes serão nossos Concrete Implementors. Para XLS, trabalharemos com o objeto TExcelApplication
.
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 |
type { Concrete Implementor } TFormatoXLS = class(TInterfacedObject, IFormato) private Excel: TExcelApplication; Linha: integer; Coluna: integer; public constructor Create; destructor Destroy; override; // métodos da Interface procedure PularLinha; procedure ExportarCampo(const Valor: string); procedure SalvarArquivo(const NomeArquivo: string); end; ... constructor TFormatoXLS.Create; begin // cria o objeto da aplicação do Excel e adiciona um novo WorkBook (planilha) Excel := TExcelApplication.Create(nil); Excel.Connect; Excel.WorkBooks.Add(xlWBATWorksheet, 0); Excel.Visible[0] := False; Linha := 1; Coluna := 1; end; destructor TFormatoXLS.Destroy; begin // "desconecta" e encerra o Excel Excel.Disconnect; Excel.Quit; FreeAndNil(Excel); inherited; end; procedure TFormatoXLS.ExportarCampo(const Valor: string); var sCelula: string; begin // encontra a célula atual (por exemplo, "A1"), para escrever o valor sCelula:= Chr(64 + Coluna) + IntToStr(Linha); Excel.Range[sCelula, sCelula].Value2 := Valor; // incrementa o contador de coluna da planilha Inc(Coluna); end; procedure TFormatoXLS.PularLinha; begin // incrementa o contador de linha da planilha e volta para a primeira coluna Inc(Linha); Coluna := 1; // auto-redimensiona as colunas Excel.Columns.AutoFit; end; procedure TFormatoXLS.SalvarArquivo(const NomeArquivo: string); var CaminhoAplicacao: string; NomeCompleto: string; begin CaminhoAplicacao := ExtractFilePath(Application.ExeName); NomeCompleto := Format('%s%s.xls', [CaminhoAplicacao, NomeArquivo]); // salva o arquivo na pasta onde está o executável Excel.ActiveWorkbook.SaveAs(NomeCompleto, xlNormal, EmptyStr, EmptyStr, False, False, xlNoChange, xlUserResolution, False, EmptyParam, EmptyParam, 0, 0); end; |
Para HTML, utilizaremos uma TStringList
nativa para armazenar as tags e os valores.
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 |
type { Concrete Implementor } TFormatoHTML = class(TInterfacedObject, IFormato) private HTML: TStringList; public constructor Create; destructor Destroy; override; procedure PularLinha; procedure ExportarCampo(const Valor: string); procedure SalvarArquivo(const NomeArquivo: string); end; ... constructor TFormatoHTML.Create; begin // cria a TStringList e já adiciona o cabeçalho HTML HTML := TStringList.Create; HTML.Add('<html>'); HTML.Add('<body>'); HTML.Add('<table border="1">'); HTML.Add('<tr>'); end; destructor TFormatoHTML.Destroy; begin FreeAndNil(HTML); inherited; end; procedure TFormatoHTML.ExportarCampo(const Valor: string); begin // cria uma nova célula na tabela e escreve o valor dentro HTML.Add(Format('<td>%s</td>', [Valor])); end; procedure TFormatoHTML.PularLinha; begin // fecha a linha atual da tabela e adiciona uma nova HTML.Add('</tr><tr>'); end; procedure TFormatoHTML.SalvarArquivo(const NomeArquivo: string); var CaminhoAplicacao: string; NomeCompleto: string; begin // fecha as Tags do HTML HTML.Add('</tr>'); HTML.Add('</table>'); HTML.Add('</body>'); HTML.Add('</html>'); // salva o arquivo na pasta onde está o executável CaminhoAplicacao := ExtractFilePath(Application.ExeName); NomeCompleto := Format('%s%s.html', [CaminhoAplicacao, NomeArquivo]); HTML.SaveToFile(NomeCompleto); end; |
Cada uma das classes acima exporta os dados conforme o tipo especÃfico de formato. Vale ressaltar que não importa a origem dos dados. O procedimento de exportação será o mesmo.
Interface Abstraction
O terceiro passo é criar a Interface do Abstraction que, no nosso caso, é o exportador:
1 2 3 4 5 |
type { Abstraction } IExportador = interface procedure ExportarDados(const Dados: olevariant); end; |
Classes Refined Abstractions
Este único método da Interface será implementado pelas nossas Refined Abstractions. Primeiro, a classe de exportação de clientes:
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 |
type TExportadorClientes = class(TInterfacedObject, IExportador) private // variável para armazenar o tipo de formato Formato: IFormato; public constructor Create(Formato: IFormato); procedure ExportarDados(const Dados: olevariant); end; ... constructor TExportadorClientes.Create(Formato: IFormato); begin // "injeta" o tipo de formato Self.Formato := Formato; end; procedure TExportadorClientes.ExportarDados(const Dados: olevariant); var cdsDados: TClientDataSet; nContador: integer; begin // cabeçalho Formato.ExportarCampo('Código'); Formato.ExportarCampo('Nome'); Formato.ExportarCampo('Cidade'); cdsDados := TClientDataSet.Create(nil); try cdsDados.Data := Dados; cdsDados.First; while not cdsDados.Eof do begin // utiliza os métodos do Implementor para realizar a exportação Formato.PularLinha; for nContador := 0 to Pred(cdsDados.Fields.Count) do Formato.ExportarCampo(cdsDados.Fields[nContador].AsString); cdsDados.Next; end; Formato.SalvarArquivo('Clientes'); finally FreeAndNil(cdsDados); end; end; |
E então, a classe de exportação de produtos:
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 |
type TExportadorProdutos = class(TInterfacedObject, IExportador) private Formato: IFormato; public constructor Create(Formato: IFormato); procedure ExportarDados(const Dados: olevariant); end; ... constructor TExportadorProdutos.Create(Formato: IFormato); begin // "injeta" o tipo de formato Self.Formato := Formato; end; procedure TExportadorProdutos.ExportarDados(const Dados: olevariant); var cdsDados: TClientDataSet; nContador: integer; begin // cabeçalho Formato.ExportarCampo('Código'); Formato.ExportarCampo('Descrição'); Formato.ExportarCampo('Estoque'); cdsDados := TClientDataSet.Create(nil); try cdsDados.Data := Dados; cdsDados.First; while not cdsDados.Eof do begin // utiliza os métodos do Implementor para realizar a exportação Formato.PularLinha; for nContador := 0 to Pred(cdsDados.Fields.Count) do Formato.ExportarCampo(cdsDados.Fields[nContador].AsString); cdsDados.Next; end; Formato.SalvarArquivo('Produtos'); finally FreeAndNil(cdsDados); end; end; |
Observe que os métodos de exportação são muito parecidos. Sendo assim, poderÃamos criar uma classe abstrata de exportação, mas, para manter o exemplo bem didático, decidi manter dessa forma.
Em ação!
Bom, talvez você ainda não tenha identificado as vantagens. Confira abaixo, por exemplo, como é feita a chamada da exportação dos dados de clientes para XLS:
1 2 3 4 5 6 7 8 9 10 |
var Exportador: IExportador; begin Exportador := TExportadorClientes.Create(TFormatoXLS.Create); try Exportador.ExportarDados(ClientDataSetClientes.Data); finally Exportador := nil; end; end; |
Notou a linha que indica que a exportação deve ser em formato XLS? Sim, através de um parâmetro na construção do Refined Abstraction (objeto de exportação):
1 |
Exportador := TExportadorClientes.Create(TFormatoXLS.Create); |
Perfeito! Se quisermos exportar para HTML, basta apenas alterar o parâmetro:
1 |
Exportador := TExportadorClientes.Create(TFormatoHTML.Create); |
E no caso dos produtos?
Mesma coisa! Instancie um exportador, informe o formato no construtor e passe os dados como parâmetro do método ExportarDados
:
1 2 3 4 5 6 7 8 9 10 11 12 |
var Exportador: IExportador; begin Exportador := TExportadorProdutos.Create(TFormatoXLS.Create); // Ou, para HTML: // Exportador := TExportadorProdutos.Create(TFormatoHTML.Create); try Exportador.ExportarDados(ClientDataSetProdutos.Data); finally Exportador := nil; end; end; |
Moleza, moleza!
Conclusão
E se surgisse a necessidade de exportação dos dados de fornecedores, como exemplifiquei no inÃcio do artigo?
Bom, a única alteração seria a criação de uma nova Refined Abstraction, chamada TExportadorFornecedores
. Só isso. Os formatos já estarão prontos! 🙂
Bom, acho que não preciso falar muito das vantagens, não é? Além de diminuir a quantidade de classes e agrupar as responsabilidades, facilitamos a manutenção evolutiva da nossa arquitetura. Quando um novo formato for adicionado, ficará “automaticamente” disponÃvel para todos os exportadores. Da mesma forma, quando um novo exportador for criado, todos os formatos já estarão disponÃveis para serem utilizados.
Ainda está com dúvidas sobre o padrão? Baixe o exemplo deste artigo no link abaixo para compreendê-lo melhor na prática. O projeto inclui algumas melhorias e refatorações não apresentadas no artigo. Aproveite para estudá-las também!
Grande abraço, pessoal!
Seus exemplos são muito bons!! Eu programo em Delphi há 13 anos, e como tenho aprendido a cada dia a respeito de patterns, interfaces e outras maravilhas. Algumas velhas amigas do delphi (como Interfaces), que a cada dia revela um ou outro segredinho. Agora você trouxe essa série de artigos sobre Design Patterns no Delphi. Ficou realmente fantástico. Eu só tenho a agradecer. E eu to curioso, você tem algum link bom para estudar isso a fundo? Quero saber tudo kk.
Bons estudos.
Olá, Ricardo, tudo bem?
Muito obrigado pelo feedback sobre os artigos! Observei, há algum tempo, que havia poucos tutoriais e exemplos de Design Patterns com Delphi e assumi esse “desafio” de apresentá-los aqui no blog. Procuro fazer o máximo para que eles fiquem bem didáticos!
A respeito dos links, tenho alguns para indicar:
Design Patterns for Humans
SourceMaking
TutorialsPoint
Na maioria das vezes estudo o conteúdo desses links para produzir os artigos. 🙂
Obrigado! Abraço!
Ótimo artigo. Parabéns pela didática !!
Muito obrigado, Carlos! 🙂
Andre, parabéns pelo trabalho. Sempre está me salvando mostrando de forma didática como resolver os problemas.
Uma pergunta: como pegar informações de um formulário HTML, aberto dentro do Delphi usando TChromium?
Olá, Victor. Obrigado pelo feedback!
Infelizmente não tenho experiência com o componente TChromium. Talvez este artigo do Amarildo Lacerda pode ajudá-lo:
http://www.tireideletra.com.br/?tag=tchromium
Abraço!
Boa tarde
Você saberia me dizer porque uma Interface desenvolvida em Delphi 7 não passa pelo Destructor da classe ?
Grato
Opa, Luiz, boa noite.
A Interface invoca o destrutor da classe quando a contagem de referência atinge zero.
Verifique se a aplicação está criando outras instâncias. Se o destrutor não está sendo chamado, provavelmente o contador de referências está maior que zero, ou seja, há outras instâncias da classe. Isso geralmente ocorre quando há uma falha na fábrica de objetos, initialization, ou um memory leak na própria classe.
Infelizmente não trabalho mais com Delphi 7, então não consigo fazer um teste, mas teoricamente o mecanismo da Interface deveria funcionar.
Abraço!
Bom dia, obrigado pela resposta.
Eu acho que é algum bug com o Delphi 7 porque a mesma codificação no Delphi XE 11 funciona normalmente, mas como é um sistema muito antigo em Delphi 7 não tem como compilar no XE 11. A interface é simples, ela cria os componentes para fazer uma requisição https e no destructor destrói os mesmos. O problema é que não passa de forma alguma pelo destructor, causando memory leak, a chamada é assim:
TApi
.New
. seta algumas informações
.
.
.Execute
Analisando o código, o problema deve ser porque dentro da interface principal tem outra interface fazendo com que o contador de referência aumente.
No Delphi XE 11 tem o atributo [weak] para isso não acontecer,mass no Delphi 7 eu desconheço.
Olá, Luiz, isso mesmo. De acordo com a publicação do Marco Cantu no link abaixo, o atributo Weak veio justamente para solucionar esse problema que você está enfrentando:
https://blog.marcocantu.com/blog/2016-april-weak-unsafe-interface-references.html
Porém, já que a migração do Delphi não é uma opção, eu sugiro que você “quebre” a chamada da requisição para que a contagem de referência faça o cálculo correto. Entendo que a sua intenção é manter o código mais Orientado a Objetos possÃvel, mas infelizmente é uma restrição técnica.
Abraço!