[Delphi] Design Patterns GoF – Strategy

[Delphi] Design Patterns - Strategy

Olá, leitores do Delphi!
O 21º artigo da série sobre Design Patterns refere-se ao padrão de projeto Strategy. A proposta deste padrão, apesar de simples, é bastante conveniente para situações em que é necessário alterar o comportamento de um mesmo objeto em tempo de execução, adquirindo uma nova “estratégia” para obter um resultado.
Não ficou muito claro? Acompanhe o artigo para conhecê-lo melhor!

 

Em função dos requisitos dos clientes, infraestrutura ou características técnicas, algumas vezes é necessário implementar diferentes rotinas que alcancem resultados semelhantes. Tome como exemplo as três formas de percorrer uma lista que publiquei há alguns meses. Podemos utilizar um For com um contador, um For-In, ou um Enumerator, porém, as três formas chegam a um mesmo resultado: acessar cada item da lista para consumir suas propriedades.
Para já entrarmos no conceito do Strategy, podemos classificar essas diferentes formas como “estratégias” de execução. Mesmo assim, em algumas literaturas sobre Design Patterns, encontrei um termo ainda mais adequado: algoritmo. O objetivo do Strategy, portanto, é permitir que uma aplicação selecione e use um algoritmo em tempo de execução de acordo com condições impostas pela regra de negócio.

Minha esposa, mais uma vez, pensando nas melhores analogias, encontrou uma que reflete bem a ideia do Strategy, referente à logins com múltiplas contas. Você certamente já deve ter utilizado algum serviço na web que permite o login utilizando a conta do Facebook, Google+ ou Twitter, não é? Os sites Pinterest, Digg e 4Shared são exemplos. A API destes provedores possuem diferentes métodos e parâmetros para acessar os dados do usuário, mas, em geral, o objetivo é o mesmo: utilizá-los para conectar o usuário ao serviço.

Exemplo de tipos de login para acessar um serviço

A imagem acima é do Digg. Podemos imaginar que cada opção corresponde a um algoritmo para efetivar o login (prefiro utilizar a palavra “imaginar” para não afirmar que o site realmente utiliza o Strategy). De acordo com a escolha do usuário, a aplicação seleciona um dos algoritmos, executa suas regras específicas para obter os dados do usuário e finalmente fornece acesso ao site. Com o Strategy, todo este procedimento acontece de forma encapsulada. O objeto que solicita os dados do usuário não conhece, de antemão, qual o provedor selecionado. Tal informação é concedida apenas em tempo de execução.

Formas de pagamento também são uma analogia. Podemos, por exemplo, optar por pagar uma compra com cartão de crédito, débito ou voucher. Cada uma dessas formas dispara uma rotina diferente na aplicação, mas com o objetivo comum de liquidar o valor da compra.

O conceito do Strategy ficará mais claro com um exemplo prático. Codificaremos um validador de endereços de e-mail que atende ao perfil de diferentes tipos de usuário, descritos a seguir.
No primeiro tipo de usuário, o preenchimento de e-mails não é tão frequente, portanto, a aplicação apenas carregará uma DLL dinamicamente (que eu já desenvolvi previamente) para validar o endereço de e-mail.
O segundo tipo de usuário solicita uma validação mais assertiva. Para isso, acessaremos o WebService do RegExLib para buscar uma expressão regular e validar o endereço de e-mail utilizando o record TRegEx nativo do Delphi.
O terceiro tipo de usuário exige que a validação seja feita por um serviço exclusivo para essa finalidade. Neste caso, faremos uso do MailBoxLayer, que disponibiliza uma API REST para notificar se o endereço de e-mail informado na requisição é válido.

Formulário para exemplo do Design Pattern Strategy

Observe que, apesar de termos três algoritmos, o objetivo é o mesmo. A diferença é que podemos alternar o algoritmo de validação em tempo de execução, a qualquer momento, sem que seja necessário modificar o código e recompilá-lo.

 

Começaremos pela Interface Strategy, que introduz os métodos que serão executados por cada algoritmo. No nosso cenário, haverá apenas uma função, responsável por receber o endereço de e-mail como parâmetro e retornar um boolean para indicar se é válido.

type
  IStrategy = interface
    function ValidarEmail(const Email: string): boolean;
  end;

 

Prosseguindo, precisamos modelar as classes Concrete Strategy, que implementam a Interface acima. Cada uma delas executará um algoritmo específico para validar o endereço de e-mail.
O primeiro Concrete Strategy carrega uma DLL chamada “ValidadorEmail.dll” dinamicamente com o método LoadLibrary. Em seguida, obtém o endereço do método ValidarEmail para executá-lo:

type
  TConcreteStrategyDLL = class(TInterfacedObject, IStrategy)
  public
    // Assinatura da Interface Strategy
    function ValidarEmail(const Email: string): boolean;
  end;
 
  // Define um tipo de função que corresponde ao método da DLL
  TValidarEmail = function(const Email: string): boolean; stdcall;
 
implementation
 
uses
  Winapi.Windows;
 
{ TConcreteStrategyDLL }
 
function TConcreteStrategyDLL.ValidarEmail(const Email: string): boolean;
var
 HandleDLL: THandle;
 ValidarEmail: TValidarEmail;
begin
  // Carrega a DLL
  HandleDLL := LoadLibrary('ValidadorEmail.dll');
  try
    // Obtém o endereço do método da DLL chamado "ValidarEmail"
    @ValidarEmail := GetProcAddress(HandleDLL, 'ValidarEmail');
 
    // Chama o método da DLL para validar o e-mail
    result := ValidarEmail(Email);
  finally
    // Descarrega a DLL
    FreeLibrary(HandleDLL);
  end;
end;

 

O segundo Concrete Strategy é um pouco mais complexo. Precisamos importar o endereço WSDL do RegExLib através do WSDL Importer do Delphi para criar uma unit com os métodos disponibilizados pelo WebService. Feito isso, instanciaremos um componente THTTPRIO para consumir o método que busca uma expressão regular pelo ID.

Que ID é esse?
No site do RegExLib, existe uma série de expressões regulares para diferentes finalidades. Cada uma delas possui um ID único para que seja possível consultá-la pelo WebService sem o risco de ambiguidades. Após analisar as expressões regulares referentes à e-mail, optei pela expressão nº 3122.
Por fim, utilizaremos o record TRegEx nativo do Delphi para confrontar o endereço de e-mail com a expressão regular:

type
  TConcreteStrategyRegExLib = class(TInterfacedObject, IStrategy)
  private
    // Acessa o WebService para consultar a expressão regular
    function ObterExpressao: string;
  public
    // Assinatura da Interface Strategy
    function ValidarEmail(const Email: string): boolean;
  end;
 
implementation
 
uses
  System.RegularExpressions, Soap.SOAPHTTPClient;
 
{ TConcreteStrategyRegExLib }
 
function TConcreteStrategyRegExLib.ObterExpressao: string;
var
  WebServiceRegExLib: WebservicesSoap;
  HTTPRIO: THTTPRIO;
  Expressao: RegExpDetails;
begin
  // Cria um objeto da classe THTTPRIO
  HTTPRIO := THTTPRIO.Create(nil);
 
  // Obtém uma instância do consumidor do WebService
  WebServiceRegExLib := GetWebservicesSoap(True, '', HTTPRIO);
 
  // Consulta os dados da expressão regular (o ID 3122 corresponde a uma validação de e-mail)
  Expressao := WebServiceRegExLib.getRegExpDetails(3122);
 
  // Obtém a string referente à expressão regular
  result := Expressao.regular_expression;
 
  // Libera o objeto da memória
  Expressao.Free;
end;
 
function TConcreteStrategyRegExLib.ValidarEmail(const Email: string): boolean;
var
  RegEx: TRegEx;
begin
  // Cria uma instância do record TRegEx informando a expressão consultada no WebService
  RegEx := TRegEx.Create(ObterExpressao);
 
  // Valida o e-mail com a expressão regular
  result := RegEx.Match(Email).Success;
end;

 

O último Concrete Strategy encaminha a validação para um serviço na web, chamado MailBoxLayer. Para enviar uma requisição à API REST deste serviço, utilizamos o componente TIdHTTP. Depois, recebemos a resposta em um objeto da classe TJSONObject para obter o valor da chave “format_valid”. Caso seja “true”, significa que o endereço de e-mail é válido.

type
  TConcreteStrategyMailBoxLayer = class(TInterfacedObject, IStrategy)
  private
    // Envia uma requisição à API do MailBoxLayer e recebe um JSON como retorno
    function ObterJSONValidacao(const Email: string): string;
  public
    // Assinatura da Interface Strategy
    function ValidarEmail(const Email: string): boolean;
  end;
 
implementation
 
uses
  System.SysUtils, System.JSON, System.Classes, IdHTTP;
 
{ TConcreteStrategyMailBoxLayer }
 
function TConcreteStrategyMailBoxLayer.ObterJSONValidacao(const Email: string): string;
var
  URL: string;
  IdHTTP: TIdHTTP;
  Resposta: TMemoryStream;
begin
  // URL que será enviada na requisição
  URL := 'https://apilayer.net/api/check?access_key=API_KEY&email=%s&format=1';
 
  // Cria um objeto da classe TIdHTTP para enviar a requisição
  IdHTTP := TIdHTTP.Create(nil);
 
  // Cria um objeto da classe TMemoryStream para receber o retorno
  Resposta := TMemoryStream.Create;
  try
    // Envia a requisição e recebe a resposta
    IdHTTP.Get(Format(URL, [Email]), Resposta);
 
    // Converte o conteúdo do objeto da classe TMemoryStream para string
    SetString(result, PAnsiChar(Resposta.Memory), Resposta.Size);
  finally
    // Libera os objetos da memória
    IdHTTP.Free;
    Resposta.Free;
  end;
end;
 
function TConcreteStrategyMailBoxLayer.ValidarEmail(const Email: string): boolean;
var
  Resposta: string;
  JSON: TJSONObject;
begin
  // Obtém a resposta JSON da chamada à API
  Resposta := ObterJSONValidacao(Email);
 
  // Cria o objeto para trabalhar com JSON
  JSON := TJSONObject.Create;
  try
    // Atribui o conteúdo JSON ao objeto
    JSON.Parse(TEncoding.ASCII.GetBytes(Resposta), 0);
 
    // Se o valor da chave "format_valid" for "true", significa que o e-mail é válido
    result := JSON.GetValue('format_valid') is TJSONTrue;
  finally
    // Libera o objeto da memória
    JSON.Free;
  end;
end;

 

Bom, as nossas “estratégias” estão prontas, pessoal!
Para finalizar, falta apenas o terceiro elemento do Strategy, chamado Context (sim, o mesmo nome que existe no State!). Essa classe será encarregada de instanciar e manter uma referência a uma das classes Concrete Strategy para, posteriormente, chamar o método de validação de e-mail correspondente.
Para a criação do Concrete Strategy em tempo de execução, julguei oportuno declarar as opções de algoritmo como tipos enumerados:

TTipoValidacao = (avDLL, avRegExLib, avMailBoxLayer);

No entanto, em um ambiente real, recomendo a implementação de uma Factory para esse propósito.
O Context disponibilizará apenas um método chamado ValidarEmail, no qual encaminhará a ação ao Concrete Strategy selecionado. Esse encapsulamento evita que o Client (classe ou formulário que consumirá o Strategy) tenha conhecimento da implementação interna das classes Concrete Strategy.

type
  TContext = class
  private
    // Variável para manter uma referência ao Concrete Strategy selecionado
    FStrategy: IStrategy;
  public
    // Método responsável por encaminhar a validação do e-mail ao Concrete Strategy
    function ValidarEmail(const TipoValidacao: TTipoValidacao; const Email: string): boolean;
  end;
 
implementation
 
{ TContext }
 
function TContext.ValidarEmail(const TipoValidacao: TTipoValidacao;
  const Email: string): boolean;
begin
  // Cria a instância de um Concrete Strategy conforme o tipo de validação selecionado
  case TipoValidacao of
    avDLL:          FStrategy := TConcreteStrategyDLL.Create;
    avRegExLib:     FStrategy := TConcreteStrategyRegExLib.Create;
    avMailBoxLayer: FStrategy := TConcreteStrategyMailBoxLayer.Create;
  end;
 
  // Chama a função "ValidarEmail" do Concrete Strategy
  result := FStrategy.ValidarEmail(Email);
end;

 

Vamos avaliar tudo isso funcionando na prática?
No formulário, precisamos criar uma instância do Context e, claro, também liberá-la da memória no destrutor:

private
  // Variável para armazenar uma instância do Context
  FContext: TContext;
end;
 
{ ... }
 
procedure TForm1.FormCreate(Sender: TObject);
begin
  // Cria a instância do Context
  FContext := TContext.Create;
end;
 
procedure TForm1.FormDestroy(Sender: TObject);
begin
  // Libera a instância do Context da memória
  FContext.Free;
end;

 

No evento de validação do e-mail (botão “Validar”), o código será pequeno, simples e prático:

var
  TipoValidacao: TTipoValidacao;
  EmailValido: boolean;
begin
  // Preenche o tipo de validação conforme seleção no ComboBox
  case ComboBox1.ItemIndex of
    0: TipoValidacao := avDLL;
    1: TipoValidacao := avRegExLib;
    2: TipoValidacao := avMailBoxLayer;
  end;
 
  // Chama o método do Context que, por sua vez, encaminha ao Concrete Strategy
  // Obs: "EditEmail" é um campo de texto para informar o e-mail a ser validado
  EmailValido := FContext.ValidarEmail(TipoValidacao, EditEmail.Text);
 
  // Apresenta o resultado na tela
  if EmailValido then
    ShowMessage('O endereço de e-mail é válido!')
  else
    ShowMessage('O endereço de e-mail está incorreto.');
end;

Perfeito, não? 🙂
Um dos pontos mais interessantes desse padrão de projeto é a capacidade de permutar os algoritmos em tempo de execução, permitindo que diferentes rotinas sejam executadas em um mesmo contexto de modo abstrato e desacoplado.
Além disso, não posso deixar de mencionar a facilidade de manutenção proporcionada por essa arquitetura. Para adicionar um novo algoritmo, basta apenas criar um novo Concrete Strategy e ajustar o Context para instanciá-lo quando necessário. E digo mais: como os algoritmos estão em classes separadas, significa que satisfazemos o princípio de Responsabilidade Única. Caso você ainda não o conheça, continue acompanhando o blog! 😀

 

O projeto de exemplo deste artigo (com algumas melhorias) está disponível para download no link abaixo:

Exemplo de Strategy com Delphi

Ou no GitHub:

https://github.com/AndreLuisCelestino/Delphi-DesignPatterns/tree/master/Strategy-AndreCelestino

 

Volto em breve com o próximo Design Pattern.
Grande abraço, leitores!


 

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

8 comentários

  1. Bom dia!
    Esses exemplos práticos são de vital importância para o aprendizado dos padrões de projeto.
    Obrigado André! Agradeça também sua esposa pela contribuição nos artigos!
    Abraço!

    1. Olá, Elton! Muito obrigado pelo comentário!
      Continuarei trabalhando nos artigos da melhor forma possível.
      Obs: minha esposa sempre me salva com as analogias! 🙂

      Abração!

  2. Grande André!

    Amigo, percebe-se que você tem muito talento para compartilhar conhecimento. Sua didática é excelente. Se você ainda não é professor, pense nisso. 😉

    Sobre o artigo, ficou muito claro com todos os exemplos que vc passou. Esse é sem dúvida um dos design patterns mais usuais, pois pode ser implementado em diversas situações.

    Parabéns para vc e sua esposa pelo excelente trabalho em equipe.

    Grande abraço, amigão!

    1. Olá, Cleo, boa noite!
      Agradeço fortemente pelo comentário. Minha esposa também pediu para agradecê-lo! 🙂
      Concordo com você. O Strategy, por propor uma arquitetura que atende à complexidade das regras de negócio, torna-se um dos padrões de projetos mais utilizados.

      Abraço!
      Obs: Ainda não tive a oportunidade de ser um professor. Quem sabe este desafio surge no futuro!

  3. Parabéns pelo artigo André, quanto à sua DLL “ValidadorEmail.dll” você fez utilizando Delphi puro? poderia fazer um artigo dessa DLL tbm! Abraços!

    1. Olá, Marcelo!
      Sim, a DLL foi feita com Delphi. 🙂
      Por coincidência, recebi um comentário há alguns dias sugerindo a elaboração de um artigo para exemplificar o desenvolvimento de DLLs com Delphi. Vou publicá-lo em algumas semanas!
      Grande abraço!

  4. Bom dia André, parabéns pelo Artigo fiquei com um dúvida sobre o comentário do Factory pelo que entendi para aplicar os Concrete ia ficar dentro da fabrica ?

    Att
    Obrigado

    1. Olá, Júnior!
      O ideal é que a Factory fique em uma classe separada, chamada TFactoryStrategy, por exemplo. Nela, haveria um método que retornaria uma instância do Concrete Strategy conforme algum parâmetro informado.
      Porém, algumas vezes, a Factory pode até mesmo ser um simples método. Neste caso, poderia ser dessa forma:

      function CriarConcreteStratgy(TipoValidacao: TTipoValidacao): IStrategy;
      begin
        case TipoValidacao of
          avDLL:          result := TConcreteStrategyDLL.Create;
          avRegExLib:     result := TConcreteStrategyRegExLib.Create;
          avMailBoxLayer: result := TConcreteStrategyMailBoxLayer.Create;
        end;
      end;

      É parecido com o que está no artigo, mas extraído para um método separado.

      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.