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?
Introdução
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 aperfeiçoar 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 extensa ou complexa, na qual consome muita memória ou afeta o desempenho da aplicação. Para isso, 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:
Analogia
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 de a 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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:
1 2 3 4 5 6 |
var Proxy: TProxy; begin Proxy := TProxy.Create; Proxy.FecharCaixa; end; |
Objeto complexo encapsulado! 🙂
Exemplo de codificação do Proxy
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.
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
.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
{ "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" } |
Interface Subject e classe Real Subject
Pois bem, devemos iniciar com o Subject, uma abstração que terá a assinatura do método do cálculo de distância:
1 2 3 4 5 6 |
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
.
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 |
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; |
Classe Proxy
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:
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 |
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.
Em ação!
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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?
Conclusão
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. Ao 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.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
result := TJSONObject( TJSONObject( TJSONArray( TJSONObject( TJSONArray( JSON.GetValue('rows') ).Items[0] ).GetValue('elements') ).Items[0] ).GetValue('distance') ).GetValue('text') .Value; |
Interessante, não?
Obrigado pela visita, pessoal! Abraço!
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.
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!
Oque continuo não entendendo , qual a finalidade da interface ( ICalculador = interface)
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!
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.
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 “[email protected]”.
Abraço!
Primeiro, parabéns! Segundo, notei que a instância da interface não é destruÃda, como isso ocorre?
Olá, Rafael! Excelente pergunta! A resposta valeria um artigo, mas vou tentar explicar em poucas palavras.
Quando declaramos e usamos uma variável do tipo de Interface, ocorre um procedimento conhecido como contagem de referência. A cada vez que essa variável recebe uma instância, esse contador de referências é incrementado. O mesmo ocorre da forma inversa: quando sai do escopo, o contador de referências é decrementado. Quando este contador atinge zero, a classe é liberada automaticamente da memória.
É por este motivo que a classe
TCalculadorProxy
herda deTInterfacedObject
. Essa herança garante que essa contagem de referências funcione da forma esperada.Abraço!
Positivo André Valeu !!!!