[Delphi] Design Patterns – Proxy

Boa noite, meus amigos!
No artigo passado, sobre o Flyweight, citei a importância do fator de desempenho em um sistema. O artigo de hoje também está relacionado à este requisito não-funcional, porém, abordando o próximo – e último – Design Pattern da família estrutural: o Proxy! Elaborei um exemplo prático bem instrutivo para apresentar as vantagens.
Vamos nessa?

 

Alguma vez você já precisou configurar o endereço do proxy nas opções de internet do Windows? Este endereço refere-se a um servidor que atua como intermediário entre a máquina cliente e a internet, aplicando filtros, analisando dados e gerenciando a complexidade das requisições. Um servidor proxy, portanto, pode aprimorar a experiência de navegação web do usuário.

O Design Pattern Proxy tem basicamente a mesma responsabilidade. Ao atuar como um intermediário entre dois lados, é capaz de aumentar o desempenho de uma rotina em uma aplicação, executando validações e tratando dados, por exemplo, de forma que contribua para essa finalidade. O Proxy geralmente é adequado para cenários em que o cliente utiliza um objeto de uma classe pesada ou complexa, na qual consome muita memória ou afeta o desempenho da aplicação. O padrão de projeto reduz essa carga gerenciando as demandas dessa classe. A imagem abaixo ilustra a principal diferença ao utilizar o Proxy:

 

Qual seria a função da camada Proxy em uma aplicação?
Muitas. O Proxy pode ser conveniente para qualquer situação em que necessita-se de um intermediário para controlar os acessos a um objeto que expõe suas complexidades. Por exemplo, imagine um objeto complexo (composto por vários objetos internos) que possui uma rotina de fechamento de caixa de uma loja. Considere também que, antes de realizar este fechamento, a rotina deve validar a permissão do usuário conectado e verificar se nenhuma estação de trabalho está com uma instância aberta do sistema. A solução seria semelhante ao código abaixo:

var
  ObjetoComplexo: TObjetoComplexo;
begin
  // Cria o objeto complexo, consumindo memória
  ObjetoComplexo := TObjetoComplexo.Create;
 
  if not ObjetoComplexo.VerificarPermissao then
    Exit;
 
  if not ObjetoComplexo.VerificarTodasInstanciasEstaoFechadas then
    Exit;
 
  // O caixa será fechado somente se passar nas duas validações acima
  ObjetoComplexo.FecharCaixa;  
end;

 

No entanto, observe que existe a possibilidade da rotina principal do objeto, que é o fechamento, não ser executada em função das validações. Neste caso, a maioria dos objetos internos (ou todos) inicializados durante a criação do objeto complexo foram desnecessários. Ao instalar um Proxy, adicionamos uma nova camada entre o cliente e o objeto complexo com a seguinte codificação:

procedure TProxy.FecharCaixa;
var
  ObjetoComplexo: TObjetoComplexo;
begin
  // método interno do proxy
  if not VerificarPermissao then
    Exit;
 
  // método interno do proxy
  if not VerificarTodasInstanciasEstaoFechadas then
    Exit;
 
  // Cria o objeto complexo somente se as validações forem satisfeitas
  ObjetoComplexo := TObjetoComplexo.Create;
 
  ObjetoComplexo.FecharCaixa;  
end;

 

O cliente, por sua vez, chamaria o Proxy da forma a seguir:

var
  Proxy: TProxy;
begin
  Proxy := TProxy.Create;
  Proxy.FecharCaixa;  
end;

 

Objeto complexo encapsulado! 🙂
Bom, pessoal, acredito que o exemplo acima já tenha transmitido a ideia básica do padrão de projeto, mas, para evidenciar as vantagens em um ambiente real, elaborei um exemplo prático envolvendo cálculo de distância entre duas cidades. Para isso, utilizaremos a API do Google Maps, enviando uma URL com os parâmetros (nome das cidades de origem e destino) e recebendo um JSON como retorno. O nosso Proxy será um agente de aprimoramento de desempenho e fará o intermédio entre o cliente e o objeto complexo. Para cada requisição na API, o Proxy armazenará o resultado em um DataSet para que, caso a mesma consulta seja realizada, os dados sejam consultados neste DataSet – que é mais rápido – ao invés de utilizar a API. Na verdade, será semelhante a um recurso de cache.

 

Espere aí… JSON? Com Delphi?!
Exato! Como bônus, apresentarei uma forma de ler dados no formato JSON com as classes nativas das versões mais recentes do Delphi (XE+), declaradas no namespace System.JSON. Por essa vocês não esperavam, hein? 🙂
O objetivo do exemplo é demonstrar a utilização do Proxy para “encapsular” o acesso ao objeto complexo, que neste contexto, é chamado de Real Subject (objeto real). O Client (consumidor da rotina) não conhecerá a classe que envia a requisição para a API. Nós apenas enviaremos os parâmetros e o Proxy controlará o acesso ao objeto real, ou seja, se o cálculo da distância já estiver no DataSet de cache, a criação do objeto real será ignorada, favorecendo o desempenho.
Apenas para título de conhecimento, a resposta da API em formato JSON possui a seguinte estrutura:

{
   "destination_addresses" : [ "Curitiba, State of Paraná, Brazil" ],
   "origin_addresses" : [ "Maringá - Floriano, Maringá - PR, Brazil" ],
   "rows" : [
      {
         "elements" : [
            {
               "distance" : {
                  "text" : "426 km",
                  "value" : 426111
               },
               "duration" : {
                  "text" : "5 hours 56 mins",
                  "value" : 21348
               },
               "status" : "OK"
            }
         ]
      }
   ],
   "status" : "OK"
}

 

André, então mostre logo esse exemplo!
Ok, desculpe-me pela enrolação! Vamos lá!
Devemos iniciar com o Subject, uma abstração que terá a assinatura do método do cálculo de distância:

type
  { Subject }
  ICalculador = interface
    // Método comum entre o Proxy e o Real Subject
    function CalcularDistancia(const Origem, Destino: string): string;
  end;

 

Em seguida, codificaremos o elemento Real Subject que, no nosso caso, será uma classe “complexa” por assumir as seguintes responsabilidades:

  • Tratar a URL de envio (encoding);
  • Enviar a URL para a API do Google Maps através de um componente TIdHTTP;
  • Receber a resposta em JSON e buscar o valor da distância utilizando um objeto da classe TJSONObject.
type
  { Real Subject }
  TCalculadorReal = class(TInterfacedObject, ICalculador)
  public
    // Método da Interface
    function CalcularDistancia(const Origem, Destino: string): string;
  end;
 
implementation
 
uses
  SysUtils, IdURI, IdHTTP, System.JSON;
 
{ TCalculadorReal }
 
function TCalculadorReal.CalcularDistancia(const Origem,
  Destino: string): string;
const
  // Endereço da API do Google Maps
  GOOGLE_MAPS_API =
    'http://maps.googleapis.com/maps/api/distancematrix/json?units=metric&origins=%s&destinations=%s';
var
  IdHTTP: TIdHTTP;
  Endereco: string;
  Resposta: string;
  // Classe para trabalhar com JSON
  JSON: TJSONObject;
begin
  // Cria o componente IdHTTP para executar a consulta na API
  IdHTTP := TIdHTTP.Create(nil);
  try
    // Configura o endereço de envio de dados para a API
    Endereco := Format(GOOGLE_MAPS_API, [Origem, Destino]);
 
    // "Codifica" a URL no formato correto (por exemplo, tratando acentos)
    Endereco := TIdURI.URLEncode(Endereco);
 
    // Recebe a resposta
    Resposta := IdHTTP.Get(Endereco);
 
    // Interpreta a resposta da API como JSON
    JSON := TJSONObject.ParseJSONValue(Resposta) as TJSONObject;
 
    // Acessa o array "rows" do JSON
    JSON := TJSONArray(JSON.GetValue('rows')).Items[0] as TJSONObject;
 
    // Acessa o array "elements" do JSON
    JSON := TJSONArray(JSON.GetValue('elements')).Items[0] as TJSONObject;
 
    // Valida o status do retorno,
    // apresentando uma exceção caso as cidades não sejam encontradas
    if (JSON.GetValue('status').ToString = '"NOT_FOUND"') or
       (JSON.GetValue('status').ToString = '"ZERO_RESULTS"') then
      raise Exception.Create('A cidade de origem ou destino não foi encontrada.');
 
    // Acessa o rótulo "distance"
    JSON := JSON.GetValue('distance') as TJSONObject;
 
    // Obtém o valor do rótulo "text"
    result := JSON.GetValue('text').Value;
  finally
    // Libera o componente IdHTTP da memória
    FreeAndNil(IdHTTP);
  end;
end;

 

O próximo passo é criar o Proxy, que controlará a criação e os acessos ao Real Subject. É importante destacar que essa classe também implementa a Interface Subject:

type
  { Proxy }
  TCalculadorProxy = class(TInterfacedObject, ICalculador)
  private
    // Armazena uma referência para o Real Subject (objeto real)
    CalculadorReal: ICalculador;
 
    // DataSet para armazenar os dados de cache
    CacheDados: TClientDataSet;
  public
    constructor Create;
 
    // Método da Interface
    function CalcularDistancia(const Origem, Destino: string): string;
  end;
 
implementation
 
uses
  DB, Variants, SysUtils, Forms, uRealSubject;
 
{ TCalculadorProxy }
 
function TCalculadorProxy.CalcularDistancia(const Origem,
  Destino: string): string;
begin
  // Verifica se o valor da distância está no DataSet de cache
  if CacheDados.Locate('Origem;Destino', VarArrayOf([Origem, Destino]), []) then
  begin
    // Se o valor estiver no DataSet, não é necessário chamar o objeto real
    result := CacheDados.FieldByName('Distancia').AsString;
    Exit;
  end;
 
  // Cria a instância do objeto real (Real Subject) caso ela ainda não exista
  if not Assigned(CalculadorReal) then
    CalculadorReal := TCalculadorReal.Create;
 
  // Chama o objeto real para obter a distância usando a API do Google Maps
  result := CalculadorReal.CalcularDistancia(Origem, Destino);
 
  // Adiciona os dados no DataSet de cache
  // para evitar uma nova requisição repetida à API, aumentando o desempenho da aplicação
  CacheDados.AppendRecord([Origem, Destino, result]);
 
  // Salva o arquivo de cache em disco
  CacheDados.SaveToFile(ExtractFilePath(Application.ExeName) + 'Cache.xml');
end;
 
constructor TCalculadorProxy.Create;
var
  ArquivoCache: string;
begin
  // Cria o DataSet de cache (tabela temporária)
  CacheDados := TClientDataSet.Create(nil);
 
  // Se o arquivo de cache existir, é carregado
  ArquivoCache := ExtractFilePath(Application.ExeName) + 'Cache.xml';
  if FileExists(ArquivoCache) then
    CacheDados.LoadFromFile(ArquivoCache)
  else
  begin
    // Caso contrário, a estrutura do DataSet é criada para ser usado pela primeira vez
    // ou a cada vez que o cache for excluído do diretório da aplicação
    CacheDados.FieldDefs.Add('Origem', ftString, 50);
    CacheDados.FieldDefs.Add('Destino', ftString, 50);
    CacheDados.FieldDefs.Add('Distancia', ftString, 10);
    CacheDados.CreateDataSet;
  end;
 
  // Desliga o log de alterações
  CacheDados.LogChanges := False;
end;

Observem que a condição para verificar se os dados existem no Dataset de cache foi inserida antes da criação do Real Subject. Em outras palavras, se essa condição for verdadeira, o Real Subject não será criado e, por consequência, não será necessário consultar a distância pela API.

Para testar o Proxy, desenhei o formulário abaixo, que será o nosso Client:

O botão “Calcular Distância” buscará os valores informados nos campos de texto e chamará o Proxy para receber a distância em quilômetros, exibindo uma mensagem para o usuário:

var
  Calculador: ICalculador;
  Origem: string;
  Destino: string;
  Distancia: string;
begin
  // Formata a origem e destino no formato "Cidade,Estado" para ser enviado na URL
  Origem := Format('%s,%s', [EditCidadeOrigem.Text, ComboBoxEstadoOrigem.Text]);
  Destino := Format('%s,%s', [EditCidadeDestino.Text, ComboBoxEstadoDestino.Text]);
 
  // Cria o Proxy
  Calculador := TCalculadorProxy.Create;
 
  // Chama o método de cálculo da distância
  Distancia := Calculador.CalcularDistancia(Origem, Destino);
 
  // Mostra uma mensagem com a distância
  ShowMessage(Format('A distância entre %s e %s é %s', [Origem, Destino, Distancia]));
end;

 

Ponto final! 🙂
Para fazer o teste de desempenho, faça a mesma consulta de distância duas vezes e observe que, na segunda vez, o retorno é mais rápido, já que o Proxy se encarrega de buscar os dados no DataSet de cache ao invés de utilizar a API. Além disso, os dados do DataSet são salvos em disco, portanto, mesmo que você reinicie a aplicação, as consultas realizadas anteriormente já estarão armazenadas! Que firmeza, hein?

Leitores, no link abaixo disponibilizo o projeto de exemplo deste artigo com algumas codificações extras. Adicionei um TMemo para registrar o histórico das consultas enquanto a aplicação está aberta e também um TRadioGroup no formulário para “ligar ou desligar” o cache. Se o usuário selecionar “Sim”, o Proxy é utilizado, caso contrário, o Real Subject será diretamente instanciado e não haverá leitura do cache. Em algumas situações, disponibilizar essa opção pode ser importante, como, por exemplo, evitar que as validações do Proxy sejam temporariamente ignoradas. É por isso que o Proxy e o Real Subject devem implementar a mesma Interface.

Exemplo de Proxy com Delphi

A maior diferença no projeto, na verdade, está na leitura dos dados JSON. Recebi uma orientação de um desenvolvedor chamado Messias Bueno (obrigado, meu caro!) para ler a distância em apenas um comando totalmente orientado a objetos:

result :=
  TJSONObject(
    TJSONObject(
      TJSONArray(
        TJSONObject(
          TJSONArray(
            JSON.GetValue('rows')
          ).Items[0]
        ).GetValue('elements')
      ).Items[0]
    ).GetValue('distance')
  ).GetValue('text')
 .Value;

Interessante, não?
No futuro, pretendo publicar um artigo exclusivamente sobre leitura de JSON no Delphi. Aguardem!

Obrigado pela visita, pessoal! Abraço!


 

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

6 comentários

  1. Olá André,

    Abrir o exemplo no Delphi xe2 e ele não reconheceu o System.JSON. Você saberia me informar qual namespace devo usar nesta versão do Delphi?

    E parabéns por mais um artigo excelente e ainda por cima usando Delphi 🙂

    Abraço.

    1. Boa noite, Marcos!
      Fico muito agradecido pelos elogios! Obrigado!
      Se não me engano, o nome do namespace no XE2 é DBXJSON. O exemplo do artigo foi desenvolvido em XE7.

      Abraço!

    1. Boa tarde, Oteniel, tudo bem?

      O padrão de projeto Proxy possui três variantes: Remote Proxy, Virtual Proxy e Protection Proxy. No artigo, exemplifiquei o Virtual Proxy, cujo objetivo é atuar como um substituto do objeto complexo (ou “pesado”). Pense no Proxy como um “porta-voz” desse objeto, disponibilizando os mesmos métodos, porém, controlando o seu acesso. “CalcularDistancia”, por exemplo, existem nas duas classes (Proxy e RealSubject) mas possuem implementações diferentes.
      As duas classes devem implementar a mesma Interface (ICalculador, no caso) principalmente pelo motivo de que o Client (cliente) deve considerar que está utilizando o Real Subject quando, na verdade, está acessando um substituto, que é o Proxy. Se as classes implementassem Interfaces diferentes, teríamos que alterar o modo como os objetos são criados.
      Imagine, por exemplo, que atualmente há vários módulos que utilizam um objeto de uma classe complexa. Para aprimorar o desempenho, podemos adicionar um Proxy que atuará como intermediário entre o módulo (cliente) e a classe complexa, reduzindo requisições, validando permissões e/ou criando objetos apenas sob demanda. Para evitar a necessidade de alterar todos os módulos, o Proxy pode implementar a mesma Interface da classe complexa, disponibilizando as mesmas chamadas e, portanto, mantendo o baixo impacto na alteração.

      Obrigado por publicar a dúvida!
      Abraço!

  2. Oi André… De vez em quando visito o seu Blog para aprender algo novo. Inclusive seu artigo sobre MVC com Delphi me ajudou a entender o conceito e também no desenvolvimento da minha aplicação para o TCC da faculdade. Fico muito grato por isso. Devido à experiência que você possui, e apesar de já haver respondido uma questão semelhante em um de suas FAQs, gostaria de obter sua opinião sobre a melhor forma de desenvolver um sistema que seria utilizado por uma matriz e duas filiais localizadas em três cidades diferentes, apesar de próximas, com o Banco de Dados lotado em um servidor na matriz.

    Grande abraço.

    1. Olá, Adalberto, tudo bem?
      Ótima pergunta. Acredito que essa seja uma dúvida bem comum no mercado de softwares.
      Bom, a resposta é bem ampla e depende de vários fatores, como o segmento de negócio, velocidade de internet, infraestrutura, quantidade de usuários, quantidade de requisições diárias e, claro, a negociação de custo com o cliente. Todo esse conjunto de elementos influencia na escolha da melhor solução.
      Por exemplo, sobre bancos de dados, há boas soluções free, como Firebird e PostgreSQL, e também ótimas soluções pagas, como SQL Server e Oracle.
      A tecnologia de desenvolvimento (como linguagem de programação) também deve ser selecionada de acordo com o tipo de plataforma do software. Não possuo conhecimentos profundos em programação Web, mas, para Desktop, eu particularmente escolheria o Delphi ou C#.

      Um dos itens mais importantes, ao meu ver, é a modelagem e arquitetura do software. Aqui no blog sempre abordo boas práticas relacionadas à Engenharia de Software que podem colaborar na elaboração de uma arquitetura sustentável e escalável, ou seja, de fácil manutenção e evolução. A partir do momento que as classes estão bem definidas, a linguagem de programação é apenas uma ferramenta de produção. É fundamental, no entanto, que o código seja escrito com profissionalismo, bom senso e responsabilidade.

      Caso queira me contar mais detalhes deste projeto, envie um e-mail para “contato@andrecelestino.com”.
      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.