[Delphi] Design Patterns – Bridge

[Delphi] Design Patterns - Bridge

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:

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:

Hierarquia de Classes sem Bridge

 

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:

Hierarquia de Classes com Bridge

 

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!

Exemplo de Bridge com Delphi

 

Grande abraço, pessoal!


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

2 comentários

  1. 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.

    1. 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!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.