[Delphi] Design Patterns GoF – Adapter

[Delphi] Design Patterns - Adapter

Saudações, pessoal!
Neste artigo, iniciaremos o estudo dos padrões de projeto da categoria “Estruturais”! Recebi bons feedbacks dos artigos relacionados aos padrões de projeto criacionais, e espero que os próximos tenham a mesma repercussão.
O primeiro deles é o Adapter. Na minha opinião, é um dos Design Patterns mais fáceis de compreender. “Bora lá”!

 

Para iniciar o texto, um fato: desde 2010, o padrão de tomadas NBR 14136 entrou em vigor no Brasil para substituir os diferentes tipos que haviam no mercado. A sua estrutura traz um terceiro pino (central) para a função de aterramento:

Padrão NBR 14136 de tomadas

Porém, na época, a grande maioria de brasileiros ainda possuíam eletrodomésticos e ferramentas com o formato antigo de tomadas e, claro, a solução não era simplesmente descartá-los e comprar novos utilitários atualizados com o novo padrão. Durante essa fase de transição, a melhor alternativa era adquirir um adaptador que convertia o velho padrão no novo:

Adaptador de tomadas

Bom, acho que só essa analogia já transmite um pouco da ideia, não é? 🙂
A função do Adapter é justamente essa: atuar como um intermediário entre duas interfaces de modo que elas possam se comunicar, ou melhor, tornem-se compatíveis.
Recordam-se do artigo “30 dias com o WebService“, que publiquei em 2012? Sei que muitos não o leram (por ser antigo), mas, no artigo, faço menção da ferramenta WSDL Importer do Delphi, que interpreta um endereço WSDL e produz uma unit com as declarações das funções que o WebService disponibiliza. Essa unit é um exemplo perfeito de Adapter. A aplicação cliente, por si só, não consegue se comunicar diretamente com o WebService, portanto, é necessário uma classe para exercer uma função de adaptador entre os dois lados.

Embora o Adapter possa ser empregado em várias situações, é comum encontrá-lo nos seguintes cenários:

  • Integração com tecnologias externas, como WebServices para consultas de dados, pagamentos ou envio de arquivos;
  • Integração entre sistemas diferentes, como aplicações desktop com aplicações web ou mobile;
  • Migração de dados, quando, por exemplo, uma empresa adquire outra do mesmo segmento e pretende unificar as bases de dados.

Geralmente, o Adapter é implementado em projetos que já estão em produção e, por algum motivo, devem “adaptar-se” a um novo ambiente, encaixando-se em uma das três situações acima.
Entrando em detalhes mais técnicos, o Adapter é formado por 4 elementos:

  • Client: cliente que irá consumir os objetos;
  • Target: define a interface que o Client usará;
  • Adaptee: consiste na classe que precisa ser adaptada;
  • Adapter: realiza a adaptação entre as interfaces.

 

Para a nossa aplicação prática do padrão, pensei em uma rotina de busca de endereço (logradouro, bairro e cidade) pelo CEP utilizando uma API de terceiros.
Antes de iniciar, ressalto que a estrutura de classes do Adapter pode parecer um pouco confusa no início, portanto, vou entrar bastante em detalhes.

Vamos considerar que, atualmente, a nossa aplicação faz uso do serviço do ViaCEP para consultar o endereço. Esse serviço permite acessar um endereço específico, informando o CEP como parâmetro, para retornar os dados em formato XML.
Primeiramente, temos uma Interface relacionada aos métodos de consulta do CEP:

type
  IWebServiceViaCEP = interface
    procedure ConsultarEnderecoWebService(const Cep: string);
    function ObterLogradouro: string;
    function ObterBairro: string;
    function ObterCidade: string;
  end;

Em seguida, temos também uma classe responsável pela comunicação com o WebService para enviar/trazer os dados, que implementa a Interface acima:

type
  TWebServiceViaCEP = class(TInterfacedObject, IWebServiceViaCEP)
  private
    XMLDocument: IXMLDocument;
  public
    constructor Create;
    destructor Destroy; override;
 
    procedure ConsultarEnderecoWebService(const Cep: string);
    function ObterLogradouro: string;
    function ObterBairro: string;
    function ObterCidade: string;    
  end;

O objeto “XMLDocument” armazena o retorno da consulta, ou melhor, os dados do endereço. Como o utilizaremos em mais de um método, achei viável declará-lo como uma variável da classe. Logo, os métodos Create e Destroy são declarados apenas para instanciar e liberar essa variável da memória:

constructor TWebServiceViaCEP.Create;
begin
  XMLDocument := TXMLDocument.Create(nil);
end;
 
destructor TWebServiceViaCEP.Destroy;
begin
  XMLDocument := nil;
  inherited;
end;

O método de consulta é bem simples. Apenas acessa o endereço do ViaCEP e atribui o resultado no objeto “XMLDocument”:

procedure TWebServiceViaCEP.ConsultarEnderecoWebService(const Cep: string);
begin
  XMLDocument.FileName := Format('https://viacep.com.br/ws/%s/xml/', [Cep]);
  XMLDocument.Active := True;
end;

Por fim, as três últimas funções acessam os Nodes (nós) do XML para obter os dados consultados:

function TWebServiceViaCEP.ObterBairro: string;
begin
  result := VarToStr(XMLDocument.DocumentElement.ChildValues['bairro']);
end;
 
function TWebServiceViaCEP.ObterCidade: string;
begin
  result := VarToStr(XMLDocument.DocumentElement.ChildValues['localidade']);
end;
 
function TWebServiceViaCEP.ObterLogradouro: string;
begin
  result := VarToStr(XMLDocument.DocumentElement.ChildValues['logradouro']);
end;

 

Até o momento, a rotina funciona perfeitamente e nenhum adaptador é necessário, portanto, a seguir, temos apenas dois elementos: o Target (agente que solicita a busca) e o Client (que consome o Target). No nosso exemplo, o Client é uma simples tela para informar o CEP. O Target, por sua vez, possui o nome de TComunicador:

type
  TComunicador = class
  private
    WebServiceViaCEP: IWebServiceViaCEP;
  public
    constructor Create(WebServiceViaCEP: IWebServiceViaCEP);
    destructor Destroy; override;
 
    function ConsultarEndereco(const Cep: string): TStringList;
  end;

Observe que o construtor recebe um objeto de uma classe que implementa a Interface IWebServiceViaCEP. Como vimos anteriormente, este objeto é o que consulta, efetivamente, o CEP no serviço. O TComunicador é apenas uma ligação entre o Client e o serviço de busca, semelhante a um Controller.

Os métodos Create e Destroy associam e desassociam, respectivamente, a variável da classe referente ao objeto da classe de consulta:

constructor TComunicador.Create(WebServiceViaCEP: IWebServiceViaCEP);
begin
  Self.WebServiceViaCEP := WebServiceViaCEP;
end;
 
destructor TComunicador.Destroy;
begin
  WebServiceViaCEP := nil;
  inherited;
end;

Por fim, há a implementação do método principal da classe, encarregado de utilizar o objeto da classe de consulta para buscar o endereço e retornar o resultado como itens de uma TStringList:

function TComunicador.ConsultarEndereco(const Cep: string): TStringList;
var
  Dados: TStringList;
begin
  WebServiceViaCEP.ConsultarEnderecoWebService(Cep);
 
  Dados := TStringList.Create;
 
  Dados.Values['Logradouro'] := WebServiceViaCEP.ObterLogradouro;
  Dados.Values['Bairro'] := WebServiceViaCEP.ObterBairro;
  Dados.Values['Cidade'] := WebServiceViaCEP.ObterCidade;
 
  result := Dados;
end;

Opa, já começou a ficar mais claro! Agora, para entendermos por completo, veremos como o Client (tela) processa essa ação:

var
  WebServiceViaCEP: TWebServiceViaCEP;
  Comunicador: TComunicador;
  Retorno: TStringList;
begin
  // instancia o objeto da classe de consulta
  WebServiceViaCEP := TWebServiceViaCEP.Create;
 
  // instancia o comunicador (Target), injetando o objeto de consulta
  Comunicador := TComunicador.Create(WebServiceViaCEP);
 
  Retorno := TStringList.Create;
  try
    // o Target consulta o endereço (utilizando o objeto injetado) e retorna os dados
    Retorno := Comunicador.ConsultarEndereco(EditCEP.Text);
 
    EditLogradouro.Text := Retorno.Values['Logradouro'];
    EditBairro.Text := Retorno.Values['Bairro'];
    EditCidade.Text := Retorno.Values['Cidade'];
  finally
    FreeAndNil(Retorno);
    FreeAndNil(Comunicador);
  end;
end;

Notou que utilizei a palavra “injetar” nos comentários? Isso é muito importante! Essa “injeção” cria uma composição de objetos para trabalharem em conjunto. Explico com mais detalhes em um artigo no futuro! 🙂

 

Pois bem, pessoal, eis que surge um cenário: recentemente, alguns clientes que usam a aplicação solicitaram que a busca do CEP passe a ser realizada pelo WebService dos Correios, e não mais pelo ViaCEP. Embora a alteração pareça simples, a busca de CEP pelos Correios é realizada por WSDL (Web Services Description Language), bem diferente do ViaCEP, que bastava apenas acessar um endereço.
Nessa situação, teremos que garantir que a implementação da rotina dos Correios não impacte na funcionalidade já existente do ViaCEP. Em outras palavras, a nossa missão é fazer com que as duas consultas se comportem da mesma maneira sem a necessidade de alterar a classe de consulta do ViaCEP (TWebServiceViaCEP). Essa é a motivação do Adapter!

Nas próximas linhas de código, você notará que faremos o Target “conversar” com o WebService dos Correios por meio de um adaptador.
Antes de prosseguir, uma nota muito importante: quando estudei o Adapter, pensei que a classe já existente que deveria ser adaptada, mas é o inverso! A nova classe é que passará por uma “adaptação” para tornar-se compatível com o ambiente atual. No nosso caso, a rotina dos Correios que será adaptada.

Bom, então, mãos à massa!
Após a importação do WSDL, o primeiro passo é analisar e criar a classe de consulta dos Correios. Neste exemplo, ela terá a seguinte estrutura:

type
  TWebServiceCorreios = class
  private
    Parametros: consultaCEP;
    Resposta: enderecoERP;
  public
    destructor Destroy; override;
    procedure DefinirParametrosConsulta(const Cep: string);
    procedure ConsultarCEP;
    function ObterResposta(const Informacao: string): string;
  end;

Observe que os métodos são bem diferentes da classe de consulta do ViaCEP. O parâmetro de consulta e o retorno são objetos ao invés de tipos primitivos.

O método “DefinirParametrosConsulta” cria um objeto que o WebService exige para consulta e o preenche com o CEP informado pelo usuário:

procedure TWebServiceCorreios.DefinirParametrosConsulta(
  const Cep: string);
begin
  Parametros := consultaCEP.Create;
  Parametros.cep := Cep;
end

Para consultar o CEP e receber a resposta, é necessário instanciar um componente do tipo THTTPRIO, responsável por produzir requisições SOAP e enviá-las ao WebService. Interface disponibilizada pelos Correios, conforme o manual de integração, chama-se “AtendeCliente”.

procedure TWebServiceCorreios.ConsultarCEP;
var
  Correios: AtendeCliente;
  HTTPRIO: THTTPRIO;
begin
  HTTPRIO := THTTPRIO.Create(nil);
  Correios := GetAtendeCliente(True, '', HTTPRIO);
  Resposta := Correios.consultaCEP(Parametros).return;
end;

A função “ObterResposta” acessa as propriedades do objeto de resposta conforme o tipo de informação solicitado no parâmetro:

function TWebServiceCorreios.ObterResposta(
  const Informacao: string): string;
begin
  result := EmptyStr;
 
  if Informacao = 'Logradouro' then
    result := Resposta.end_
  else if Informacao = 'Bairro' then
    result := Resposta.bairro
  else if Informacao = 'Cidade' then
    result := Resposta.cidade;
end;

E o Destroy, por fim, libera o objeto de parâmetro da memória:

destructor TWebServiceCorreios.Destroy;
begin
  FreeAndNil(Parametros);
  inherited;
end;

 

Claro, poderíamos criar um novo comunicador exclusivo para os Correios, mas, neste caso, estaríamos aumentando a complexidade ciclomática da nossa aplicação. Além disso, imagine que essa rotina seja empregada em várias partes da aplicação, como cadastro de clientes, fornecedores, parceiros, funcionários e filiais. A nossa intenção é modificar o mínimo possível do código para não comprometer o funcionamento em cada um desses locais.

O próximo passo é implementar a solução do problema: o Adapter! A classe abaixo adaptará o objeto da classe dos Correios para torná-la compatível com a classe do ViaCEP. Logo, a classe dos Correios é o nosso Adaptee.

type
  TAdapter = class(TInterfacedObject, IWebServiceViaCEP)
  private
    WebServiceCorreios: TWebServiceCorreios;
  public
    constructor Create(WebServiceCorreios: TWebServiceCorreios);
    procedure ConsultarEnderecoWebService(const Cep: string);
    function ObterLogradouro: string;
    function ObterBairro: string;
    function ObterCidade: string;       
  end;

Notou algo familiar?
A classe implementa a Interface IWebServiceViaCEP, a mesma utilizada para a classe de consulta do ViaCEP, apenas com uma particularidade: ela recebe um objeto de consulta dos Correios no construtor:

constructor TAdapter.Create(WebServiceCorreios: TWebServiceCorreios);
begin
  Self.WebServiceCorreios := WebServiceCorreios;
end;

Agora, preste atenção na implementação de cada método:

procedure TAdapter.ConsultarEnderecoWebService(const Cep: string);
begin
  WebServiceCorreios.DefinirParametrosConsulta(Cep);
  WebServiceCorreios.ConsultarCEP;
end;
 
function TAdapter.ObterBairro: string;
begin
  result := WebServiceCorreios.ObterResposta('Bairro');
end;
 
function TAdapter.ObterCidade: string;
begin
  result := WebServiceCorreios.ObterResposta('Cidade');
end;
 
function TAdapter.ObterLogradouro: string;
begin
  result := WebServiceCorreios.ObterResposta('Logradouro');
end;

Agora ficou claríssimo! O adaptador “simula” a classe do ViaCEP, com os mesmos nomes de métodos, porém, utiliza o objeto da classe dos Correios para realizar a busca e obter o retorno.
Vamos tirar a prova? Veja a diferença do método “ConsultarEnderecoWebService” entre as duas classes abaixo. Embora realizem ações diferentes, o nome do método que será consumido pelo Target é o mesmo.

// TWebServiceViaCEP
procedure TWebServiceViaCEP.ConsultarEnderecoWebService(const Cep: string);
begin
  XMLDocument.FileName := Format('https://viacep.com.br/ws/%s/xml/', [Cep]);
  XMLDocument.Active := True;
end;
 
// TAdapter (Correios)
procedure TAdapter.ConsultarEnderecoWebService(const Cep: string);
begin
  WebServiceCorreios.DefinirParametrosConsulta(Cep);
  WebServiceCorreios.ConsultarCEP;
end;

 

Entendi, mas como fica o Client?
Legal, chegamos na última parte! A chamada no Client sofre uma pequena alteração. Ao invés de injetarmos um objeto do tipo TWebServiceViaCEP, injetaremos um objeto do tipo TAdapter, já que implementam a mesma Interface:

var
  WebServiceCorreios: TWebServiceCorreios;
  Adapter: TAdapter;
  Comunicador: TComunicador;
  Retorno: TStringList;
begin
  // instancia o objeto de consulta dos Correios (Adaptee)
  WebServiceCorreios := TWebServiceCorreios.Create;
 
  // instancia o Adaptador (Adapter)
  Adapter := TAdapter.Create(WebServiceCorreios);
 
  // instancia o comunicador (Target), injetando o adaptador
  Comunicador := TComunicador.Create(Adapter);
 
  // as próximas são linhas são idênticas...
  Retorno := TStringList.Create;
  try
    Retorno := Comunicador.ConsultarEndereco(EditCEP.Text);
 
    EditLogradouro.Text := Retorno.Values['Logradouro'];
    EditBairro.Text := Retorno.Values['Bairro'];
    EditCidade.Text := Retorno.Values['Cidade'];
  finally
    FreeAndNil(Retorno);
    FreeAndNil(Comunicador);
  end;
end;

O que mudou no Client? Adicionamos apenas 1 linha, referente ao adaptador. Todo o resto é idêntico, salvo que instanciamos um objeto da classe dos Correios ao invés do ViaCEP. Viram como é interessante?

 

André, é muita coisa!
Sim, concordo. O conceito do Adapter é de fácil compreensão, mas a implementação tem uma complexidade um pouco mais alta. No entanto, as vantagens são evidentes:

  • A classe com funcionamento já consolidado (ViaCEP) não foi modificada;
  • Adicionamos apenas 1 linha no método original;
  • Podemos criar outros adaptadores conforme necessário (por exemplo, Postmon ou BuscarCEP);
  • Todos os adaptadores implementarão uma única Interface.

Bom, leitores, entendo que o exemplo ficou bastante extenso. Por isso, recomendo que vocês baixem o exemplo no link abaixo e veja o Adapter funcionando na prática! A tela principal do projeto traz 2 botões: o primeiro aciona a consulta pelo ViaCEP e o segundo utiliza o WebService dos Correios através de um adaptador. De qualquer forma, deixei o código bem documentado para uma melhor compreensão.
Ah, uma observação: não sei o motivo, mas, no projeto de exemplo, o request gerado pelo componente THTTPRIO retorna um erro ao ser processado. Por conta disso, tive de sobrescrever a requisição SOAP originalmente gerada pelo componente THTTPRIO, adaptando-a para a estrutura exigida pelo WebService dos Correios.

Exemplo de Adapter com Delphi

 

Antes de encerrar o artigo, gostaria de compartilhar um comentário. Certa vez, um colega de trabalho perguntou: “Podemos dizer que o Adapter é uma solução paliativa, não é? O ideal não seria remodelar a modelagem de classes para atender aos novos padrões?”. Bom, concordo em partes. Usando a analogia dos padrões de tomadas, praticamente todos os brasileiros já trocaram de eletrodomésticos hoje em dia, uma vez que o padrão entrou em vigor há 6 anos, portanto, os adaptadores estão gradativamente caindo em desuso, salvo exceções. Seguindo este mesmo raciocínio, classes adaptadoras também podem se tornar obsoletas. Neste caso, sim, uma remodelagem é uma ação factível.
No entanto, discordo da afirmação de que este Design Pattern é um paliativo. Pelo contrário. O Adapter é uma solução sólida e pontual para um problema de incompatibilidade no sistema. Sendo assim, enquanto essa incompatibilidade existir, o Adapter , ao meu ver, é uma das melhores alternativas.

 

Hora de partir, leitores!
Vejo vocês no artigo do próximo padrão de projeto estrutural!


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

10 comentários

  1. Amigo André, como sempre artigos muito claros e bem explicados. Parabéns. Gostaria de sugerir o padrão Bridge, também da GoF. E o “Dependency Injection”, pois vejo que os Delpheiros pouco sabem a respeito e é um mundo muito amplo no mercado Java e Android. Injeção de Dependência pode trazer ganho muito grande aos aplicativos principalmente com desacoplamento permitindo, por exemplo, possibilitando mock das classes para fins de testes automatizados.

    1. Isso aí, Marcos! Dependency Injection é um grande aliado do baixo acoplamento. Se este padrão estivesse no mindset de todo desenvolvedor, a taxa de manutenção e custo de evolução não seriam tão altos quanto são atualmente. Espero que este conceito seja cada vez mais disseminado no cenário de desenvolvimento…
      Ah, e por coincidência, o padrão Bridge é o tema do próximo artigo! 🙂

      Grande abraço!

  2. Boa tarde André.
    Muito bom o seu exemplo.
    Estou com um erro na unit webservicecorreios, nessa linha:

    HTTPRIO.OnBeforeExecute := BeforeExecute;

    o erro é Incompatible types: ‘Parameter lists differ’

    Sabe me dizer o que pode estar causando?

    1. Olá, Renan!
      O projeto de exemplo que anexei no artigo foi desenvolvido na versão Berlin. Nas versões anteriores, o evento BeforeExecute tem uma assinatura de parâmetros diferente, referente ao tipo do segundo parâmetro:

      BeforeExecute(const MethodName: String; var SOAPRequest: WideString);

      Se você for abrir o projeto nessas versões, terá que modificar este evento para compilá-lo.

      Abraço!

  3. Olá André.

    Obrigado pela resposta.
    Estou tentando na versão 10.2 Tokyo.
    Será que mudou algo da Berlin para esta?

    1. Tem razão, Renan.
      Quando migrei o exemplo do Delphi 7 para o Berlin, esqueci de atualizar o projeto em anexo.
      Agora está correto. Peço que baixe-o novamente.

      Abraço!

  4. Bom dia André.
    Estava implantando a sua solução no meu sistema e me deparei com 2 situações.
    Fiz a busca no evento de exit do edCEP.Text

    1- Caso não tenha internet, obtenho um erro. Poderíamos tratar esse erro e apresentar uma mensagem amigavel ou ainda, permitir que o usuario digite manualmente em caso de erro?

    2- Tem como usar as 2 consultas ao mesmo tempo?
    Ex: Caso falhe VIACEP, automaticamente tentar CORREIOS?

    Obrigado

    1. PS: o tratamento de exceção já tinha no botão dos correios, então não precisa.

    2. Olá, Renan!
      Se fizermos esse controle, de usar as duas classes ao mesmo tempo, fugiremos da proposta do Adapter. A intenção é que apenas uma delas seja utilizada no cliente, adaptada quando necessário. Em outras palavras, o objetivo do padrão de projeto é adaptar uma funcionalidade conforme a realidade existente. No artigo, essa adaptação é o provedor de busca de endereço pelo CEP.

      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.