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!
Introdução
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.
Analogias
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.
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.
Exemplo de codificação do Strategy
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.
O 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 esse exemplo) 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.
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.
Interface Strategy
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.
1 2 3 4 |
type IStrategy = interface function ValidarEmail(const Email: string): boolean; end; |
Classe Concrete Strategy
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:
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 |
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.
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:
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 |
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.
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 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!
Classe Context
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:
1 |
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.
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 |
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; |
Em ação!
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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? 🙂
Conclusã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:
Volto em breve com o próximo Design Pattern.
Grande abraço, leitores!
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!
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!
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!
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!
Parabéns pelo artigo André, quanto à sua DLL “ValidadorEmail.dll” você fez utilizando Delphi puro? poderia fazer um artigo dessa DLL tbm! Abraços!
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!
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
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:
É parecido com o que está no artigo, mas extraído para um método separado.
Abraço!
André, boa tarde!
Para reduzir IF e Case na minha camada de estratégia eu faço conforme exemplo abaixo, o que você acha?
Crio um dicionário de Objetos com base numa interface, instancio todas as classes através do método New que retorna sempre Self.Create sendo que cada classe implementa a Interface iRegras.
No Destroy eu destruo o ObjectDicionary da seguinte forma:
E para chamar as regras eu procedo assim neste exemplo onde LCFOPIni é apenas o primeiro caracter da CFOP, ou seja, 1,2,3,5,6,7
Por gentileza gostaria de sua opinião, pois seu artigos são sempre bem elucidativos e muito explicados, minha abordagem está correta? Ou você tem alguma sugestão melhor?
Grande abraço.
Ronaldo Jorge
Olá, Ronaldo, bom dia!
Primeiramente, obrigado pelo feedback sobre os artigos do blog!
Em segundo lugar, parabéns, Ronaldo! A sua solução ficou fantástica, na minha opinião, por esses motivos:
– Uso de Interfaces (abstração)
– Uso do constructor “New”
– Uso de dicionário de objetos (que é mais performático do que outras listas)
– Eliminação de If/Else
– Implementação de padrão de projeto
Quem nos dera se todo desenvolvedor tivesse essa sensibilidade técnica ao desenvolver 😀
Ficou muito bom! Grande abraço!
André, boa tarde!
Puxa vida fiquei realmente muito feliz com elogio vindo de um profissional tão qualificado como você.
Grande abraços.