Design Patterns: Strategy

Flavio Ribeiro Lima
5 min readFeb 24, 2022

--

O pattern Strategy está na categoria behavioral patterns do GOF, é um padrão comportamental.

Intenção

Definir uma família de algoritmos, encapsular cada uma delas e torná-las intercambiáveis. Strategy permite que o algoritmo varie independentemente dos clientes que o utilizam.

Por que usar o Strategy?

Em muitas situações não é uma boa ideia ter uma classe fixa para resolver um problema, e em outras realmente temos a necessidade de escolher qual classe será selecionada para resolver um dado problema.

Temos a necessidade de incluir facilmente novas classes no conjunto para resolver novos problemas, mantendo o código estável.

Quando utilizar?

  • Muitas classes relacionadas diferem somente no seu comportamento. O Strategy fornece uma maneira de escolher uma classe dentre várias.
  • Você necessita de várias soluções para o mesmo problema.
  • Uma solução usa dados dos quais os clientes dos objetos não deveriam ter conhecimento. Use o Strategy para evitar a exposição das estruturas de dados complexas.
  • Se você estiver utilizando muitos comandos condicionais (if) no seu código. Ao invés de fazer isso, mova cada regra em uma classe separada.

Participantes

  • Strategy: Uma interface comum para todas as classes suportadas. Na estrutura abaixo, a classe Context usa essa interface para chamar um determinado ConcreteStrategy
  • ConcreteStrategy: Implementa uma solução especifica do seu problema.
  • Context: Utiliza objetos ConcreteStrategy, tem dependência do objeto Strategy.

Funcionamento

Na estrutura, Context e Strategy interagem para implementar o objeto escolhido. O Context informa todos os dados requeridos pelos objetos ConcreteStrategy. E podemos passar o próprio Context para os ConcreteStrategy, permitido chama-lo de volta, se for necessário.

Consequências

  • Você pode ter uma família de classes e objetos relacionados.
  • Strategy elimina comandos condicionais. Utilize o padrão quando você tiver muitos if em seu código ou uso extenso de switch.
  • Strategy pode fornecer diferentes implementações do mesmo comportamento.
  • O padrão tem uma deficiência, onde o cliente do objeto deve conhecer os ConcreteStrategy para que selecione o mais apropriado.
  • Pode ocorrer também que determinado ConcreteStrategy não utilize a informação passada por parâmetro pelo Context, então você terá parâmetros que nunca serão utilizados.
  • Outra consequência é o aumento do número de objetos numa aplicação. Mas isso é melhor do que ter um código com uma complexidade ciclomática alta.

Implementação

Basicamente iremos utilizar uma interface para definir a assinatura do comportamento do ConcreteStrategy. Podemos utilizar Generics no C# para tornar isso ainda mais flexível, tornando o Context mais configurável.

Outra ideia que gosto, é armazenar os ConcreteStrategy numa coleção, e utilizando lambda, por ex., selecionar o objeto desejado. Com isso você pode fornecer um método em Context para permitir que o cliente adicione os ConcreteStrategy desejados e através de um parâmetro, que também pode ser definido com Generics, o cliente selecionar o que desejar.

Estrutura

Vamos ver como fica o código dessa estrutura em C#:

Considerações

Você deve estar se perguntando, “mas isso é apenas a escolha de uma classe em Context, que é possível por que Context depende de uma interface, faço isso sempre”. Exatamente, nós de alguma forma já aplicamos o Strategy no dia-a-dia. Mas nos esquecemos que podemos usar esse padrão para resolver outros problemas.

Imagine que você esteja desenvolvendo uma calculadora, sim, simples assim, sem considerar a infinidade de formas que isso possa ser feito, a primeira coisa que pensaríamos em fazer é resolver esse problema da seguinte forma:

É simples, é fácil, é prático…. para uma calculadora. Quantas vezes escrevemos muitos IF’s para resolver problemas de negócio complexos, então nosso código fica… complexo, com difícil manutenção e evolução… Ah! surgiu mais uma condição no negócio, bora lá incluir mais um IF no código, testar tudo de novo, se tiver teste unitário, mas ele também deve ser refatorado para testar a nova condição.

É ai que entra o Strategy, vamos seguir com esse simples exemplo da calculadora, o importante é absorver a ideia e poder aplicar em qualquer cenário.

Aqui basicamente a ideia é criar um ConcreteStrategy para cada operação, adicionar numa lista, e com lambda selecionar o objeto desejado. No Context podemos disponibilizar essas operações e outras.

Veja abaixo: Obs: Aqui já renomeie o Context para Calculadora, e os ConcreteStrategy para as operações.

Cade os IF’s ? sumiu!!! :-). A condição que seleciona o operação correta, é feita no Context (nossa Calculadora) (linha 53).

Demos um salto, mas o projeto ficou digamos prolixo, podemos fazer as coisas de uma forma mais direta, como por ex., fazer as inclusões das operações dentro da Calculadora, o que faz muito sentido.

Estamos usando um Get e Set para selecionar e marcar a operação desejada (linha 26, 27), podemos também fazer isso dentro da Calculadora, podemos pegar a operação desejada no construtor, e ao calcular já saberemos qual operação utilizar, deixando o código mais encapsulado.

Uma coisa que deve ter percebido é que estamos “presos” nos data types, int sendo usado como retorno, string para realizar a seleção da operação.

Por isso vamos adicionar o poder do Generics, e tornar a Calculadora, não apenas uma calculadora, mas um projeto reutilizável.

Aqui cabe uma decisão de projeto 1) Decidir o que o projeto será, e fazer a adição das operações (ConcreteStrategy) dentro do Context ou 2) Deixar o cliente adicionar as operações no Dictionary do Context, deixando-o reutilizável.

Gosto da ideia de deixar o Context reutilizável, deixando-o como um componente, então bastaria apenas que o cliente implementasse uma classe para adicionar seus próprios ConcreteStrategy. Mas com isso, será necessário disponibilizar um método para selecionar o ConcreteStrategy desejado, e teremos mais uma classe no projeto.

Veja abaixo:

Primeiro passo é definir a interface do Strategy:

Implementamos a interface para criar nosso ConcreteStrategy, que no nosso caso é classe Sum, faça essa implementação para cada operação. Perceba também que no parâmetro estou usando uma struct, mas poderia ser uma classe, ou outro tipo.

Temos então a implementação do Context, onde o TResponse será o retorno que queremos para nosso ConcreteStrategy, TResquest para o tipo do parâmetro, e TSearch para o tipo do dado que será usado para localizar um ConcreteStrategy especifico. Usamos um Dictionary para armazenar nossos ConcreteStrategy. No método Execute() utilizamos Reflection para executar o método do ConcreteStrategy.

Abaixo temos a implementação da nossa calculadora, representando nosso cliente do padrão Strategy com um objetivo especifico.

É aqui que você adiciona os comportamentos do Strategy, os ConcreteStrategy onde você desenvolve suas regras de negócio.

Veja abaixo como fica o cliente do padrão que está encapsulado na CalculadoraStrategy. Um código bem encapsulado e que pode ser facilmente estendido. Para regras de negócio complexas isso pode ser uma boa ideia, evita IF’s espalhados pelo código e separa cada problema numa classe especifica.

Para ver o completo, veja em: https://github.com/flaviorl-net/StrategyPatternCsharp

Então é isso pessoal, considere a utilização do Strategy quando tiver muitos IF’s no código e fazer uma separação bem encapsulada dos objetos.

Por hora é isso, caso tenha chegado até aqui, espero que tenha gostado e aproveitado.

--

--

Flavio Ribeiro Lima

Desenvolvedor de Software. Entusiasta de boas práticas, padrões de desenvolvimento e arquitetura de sistemas.