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 de verdade liga duas cidades.
Bom, não é bem isso. Acompanhe o artigo e conheça a ideia por trás deste padrã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.
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 importá-lo 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.
Â
Qual solução podemos aplicar nesse cenário?
Claro, implementando 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.
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?
Então, mãos à obra! Em primeiro lugar, criaremos a Interface do Implementor, que se refere ao tipo de formato:
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;
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.
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.
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.
O terceiro passo é criar a Interface do Abstraction que, no nosso caso, é o exportador:
type
{ Abstraction }
IExportador = interface
procedure ExportarDados(const Dados: olevariant);
end;
Este único método da Interface será implementado pelas nossas Refined Abstractions. Primeiro, a classe de exportação de clientes:
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:
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.
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:
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):
Exportador := TExportadorClientes.Create(TFormatoXLS.Create);
Perfeito! Se quisermos exportar para HTML, basta apenas alterar o parâmetro:
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”:
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!
André, e se surgir a necessidade de exportação dos dados de fornecedores, assim como você exemplificou no inÃcio do artigo?
A única alteração será 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!