Vantagens do padrão de estratégia

15

Por que é benéfico usar o padrão de estratégia se você pode apenas escrever seu código em casos if / then?

Por exemplo: eu tenho uma classe TaxPayer e um de seus métodos calcula os impostos usando algoritmos diferentes. Então, por que ele não pode ter casos if / then e descobrir qual algoritmo usar nesse método, em vez de usar o padrão de estratégia? Além disso, por que você não pode simplesmente implementar um método separado para cada algoritmo na classe TaxPayer?

Além disso, o que significa o algoritmo mudar no tempo de execução?

Armon Safai
fonte
2
Isso é lição de casa? É melhor afirmar isso com antecedência.
Fuhrmanator
2
@Fuhrmanator no it isnt
Armon Safai

Respostas:

20

Por um lado, grandes aglomerados de if/elseblocos não são facilmente testáveis . Cada nova "ramificação" adiciona outro caminho de execução e, portanto, aumenta a complexidade ciclomática . Se você quiser testar seu código completamente, precisará cobrir todos os caminhos de execução, e cada condição exigiria que você escrevesse pelo menos mais um teste (supondo que você escreva pequenos testes focados). Por outro lado, as classes que implementam estratégias normalmente expõem apenas um método público, fácil de testar.

Portanto, com o aninhado, if/elsevocê terminará com muitos testes para uma única parte do seu código, enquanto com o Strategy terá poucos testes para cada uma das várias estratégias mais simples. Com o último, é fácil ter uma cobertura melhor, porque é mais difícil perder os caminhos de execução.

Quanto à extensibilidade , imagine que você esteja escrevendo uma estrutura, na qual os usuários possam injetar seu próprio comportamento. Por exemplo, você deseja criar algum tipo de estrutura de cálculos de impostos e deseja oferecer suporte a sistemas tributários de diferentes países. Em vez de implementar todos eles, você só quer dar aos usuários da estrutura a chance de fornecer uma implementação de como calcular alguns impostos específicos.

Aqui está o padrão de estratégia:

  • Você define uma interface, por exemplo TaxCalculation, e sua estrutura aceita instâncias desse tipo para calcular impostos
  • Um usuário da estrutura cria uma classe que implementa essa interface e a passa para sua estrutura, fornecendo assim uma maneira de executar parte dos cálculos

Você não pode fazer o mesmo com isso if/else, porque isso exigiria a alteração do código da estrutura, caso em que não seria mais uma estrutura. Como as estruturas são frequentemente distribuídas na forma compilada, essa pode ser a única opção.

Ainda assim, mesmo se você escrever algum código comum, a Strategy é benéfica porque torna suas intenções mais claras. Ele diz "essa lógica é conectável e condicional", ou seja, pode haver várias implementações que podem variar dependendo das ações do usuário, configuração ou mesmo da plataforma.

O uso do padrão Estratégia pode melhorar a legibilidade porque, enquanto uma classe que implementa alguma estratégia específica normalmente deve ter um nome descritivo, por exemplo USAIncomeTaxCalculator, os if/elseblocos são "sem nome", na melhor das hipóteses, apenas comentados e os comentários podem mentir. Além disso, pelo meu gosto pessoal, apenas ter mais de 3 if/elseblocos seguidos não é legível e fica muito ruim com blocos aninhados.

O princípio Aberto / Fechado também é muito relevante, porque, como descrevi no exemplo acima, o Strategy permite que você estenda uma lógica em algumas partes do seu código ("aberto para extensão") sem reescrever essas partes ("fechado para modificação" )

scriptin
fonte
1
if/elseblocos também diminuem a legibilidade do código. Quanto ao padrão de estratégia, vale a pena mencionar o princípio Aberto / Fechado da IMO.
Maciej Chałapuk
1
A testabilidade é o grande motivo. (A maioria) Todos os ramos do seu código devem ser testados. Quanto mais ifvocê tiver, mais caminhos possíveis existirão no seu código, mais testes você precisará escrever e mais maneiras de esse método falhar. Posso citar o falecido Yogi Berra: "Se você chegar a uma bifurcação na estrada, pegue-a". Isso se aplica brilhantemente ao teste de unidade. Além disso, muitas ifinstruções significam que você provavelmente repetirá a lógica para essas condições, aumentando ainda mais sua carga de teste e aumentando o risco de erros surgirem.
precisa
obrigado pela resposta. Então, por que não posso usar métodos separados para algoritmos diferentes na mesma classe?
Armon Safai
Você pode, mas ainda precisaria de vários if/elseblocos para chamá-los (ou dentro deles, para determinar se deve fazer algo ou não), por isso não é de muita ajuda, exceto talvez um código mais legível. E também não há extensibilidade para usuários de sua estrutura hipotética.
Script #
1
Você pode ser mais claro sobre por que é mais fácil testar? O exemplo para refatorar uma declaração de caso (ou se / então) para um método polimórfico (base da estratégia) é bastante fácil de testar. refactoring.com/catalog/replaceConditionalWithPolymorphism.html Se conheço todas as condições para testar, escrevo um teste para cada uma. Se tenho estratégias, preciso instanciar e executar uma para cada uma. Como é mais fácil testar a abordagem estratégica? Não estamos falando de ifs aninhados complexos quando você refatorar a estratégia.
Fuhrmanator 16/11/2015
5

Por que é benéfico usar o padrão de estratégia se você pode apenas escrever seu código em casos if / then?

Às vezes você deve apenas usar se / então. É um código simples e fácil de ler.

Os dois principais problemas do código if / then simples é que ele pode violar o princípio de aberto e fechado . Se você precisar adicionar ou alterar uma condição, está modificando este código. Se você espera ter mais condições, apenas adicionar uma nova estratégia é mais simples / mais limpa / menos provável de quebrar coisas.

O outro problema é o acoplamento. Usando if / then, todas as implementações estão vinculadas a essa implementação, dificultando a mudança no futuro. Ao usar a estratégia, o único acoplamento é a interface da estratégia.

Telastyn
fonte
o que há de errado em modificar o código no código if / then? você não precisaria modificar o código no padrão de estratégia também se decidir mudar como um dos algoritmos funciona?
Armon Safai
@armonsafai - se você modificar a estratégia, basta testá-la. Se você modificar todos os algoritmos, precisará testar todos os algoritmos. Pior, se você adicionar uma nova estratégia, basta testá-la. Se você adicionar uma nova condicional, precisará testar todas as condicionais.
Telastyn
4

A estratégia é útil quando as if/thencondições são baseadas em tipos , conforme explicado em http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html

Os condicionais de verificação de tipo geralmente não têm uma alta complexidade ciclomática, portanto, não diria que a Strategy melhore as coisas necessariamente lá.

A principal razão da estratégia é explicada no livro do GoF, p.316, que introduziu o padrão:

Use o padrão de estratégia quando

...

  • uma classe define muitos comportamentos, e estes aparecem como várias instruções condicionais em suas operações. Em vez de muitos condicionais, mova os ramos condicionais relacionados para sua própria classe Strategy.

Conforme mencionado em outras respostas, se aplicado adequadamente, o padrão Estratégia permite adicionar novas extensões (estratégias concretas) sem exigir necessariamente modificações no restante do código. Esse é o chamado princípio de aberto-fechado ou princípio de variações protegidas . Obviamente, você ainda precisa codificar a nova estratégia concreta, e o código do cliente precisa reconhecer as estratégias como plug-ins (isso não é trivial).

Com if/thencondicionais, é necessário alterar o código da classe que contém a lógica condicional. Conforme mencionado em outras respostas, às vezes isso é aceitável quando você não deseja adicionar a complexidade para oferecer suporte à adição de novas funcionalidades (plug-ins) sem precisar compilar novamente.

Fuhrmanator
fonte
3

[...] se você pode simplesmente escrever seu código em casos if / then?

Esse é exatamente o maior benefício do padrão estratégico. Não tendo condições.

Você deseja que suas classes / métodos / funções sejam o mais simples e curtas possível. O código curto é muito fácil de testar e muito fácil de ler.

As condições ( if/ elseif/ else) prolongam suas classes / métodos / funções, porque geralmente o código em que uma decisão é avaliada trueé diferente da parte em que a decisão é avaliada false.


Outro grande benefício do padrão de estratégia é que ele é reutilizável em todo o projeto.

Ao usar o padrão de design da estratégia, é muito provável que você tenha algum tipo de contêiner de IoC, do qual está obtendo a implementação desejada de uma interface, talvez por um getById(int id)método, no qual o idmembro possa ser um enumerador.

Isso significa que a criação da implementação está apenas em um lugar do seu código.

Se você deseja adicionar mais implementações, adicione a nova implementação ao getByIdmétodo e essa alteração será refletida em todos os lugares do código em que você o chama.

Com if/ elseif/ elseisso é impossível de fazer. Ao adicionar uma nova implementação, você deve adicionar um novo elseifbloco e fazê-lo em qualquer lugar onde as implementações foram usadas, ou você pode acabar com um código inválido, porque esqueceu de adicionar a implementação à sua estrutura.


Além disso, o que significa o algoritmo mudar no tempo de execução?

No meu exemplo, o idpoderia ser uma variável que é preenchida com base em uma entrada do usuário. Se o usuário clicar em um botão A, então id = 2, se ele clicar em um botão B, então id = 8.

Devido ao idvalor diferente , uma implementação diferente de uma interface é obtida no contêiner IoC e o código executa operações diferentes.

Andy
fonte
obrigado pela resposta. Então, por que não posso usar métodos separados para algoritmos diferentes na mesma classe?
Armon Safai
@ArmonSafai Os métodos separados realmente resolveriam alguma coisa? Acho que não. Você está movendo o problema de um lugar para outro, e a decisão de qual método chamar será tomada com base no resultado de uma condição. Mais uma vez, um if/ elseif/ elseestado. O mesmo de antes, apenas em um lugar diferente.
Andy
Então, os casos if / then estariam no principal, certo? Você também não precisaria usar casos if / then para o padrão de estratégia?
Armon Safai
1
@ArmonSafai Não, você não faria. Você ativaria a idvariável no getByIdmétodo, que retornaria a implementação específica. Toda vez que você precisaria de uma implementação da interface, solicitaria ao contêiner de IoC que o entregasse a você.
Andy
1
@ArmonSafai Você também poderia ter um método getSortByEnumType(SortEnum type)retornando uma implementação de uma Sortinterface, um método getSortTyperetornando uma SortEnumvariável e tendo uma coleção como parâmetro, e o getSortByEnumTypemétodo conteria novamente uma opção no typeparâmetro, retornando o algoritmo de classificação correto. Se você precisar adicionar um novo algoritmo de classificação, precisará editar apenas a enumeração e um método. E você está pronto.
Andy
2

Por que é benéfico usar o padrão de estratégia se você pode apenas escrever seu código em casos if / then?

O Padrão de Estratégia permite separar seus algoritmos (os detalhes) da lógica de negócios (política de alto nível). Essas duas coisas não são apenas confusas para ler quando misturadas, mas também têm motivos muito diferentes para mudar.

Há também um fator importante de escalabilidade do trabalho em equipe aqui. Imagine uma grande equipe de programação em que muitas pessoas estão trabalhando neste pacote de contabilidade. Se os algoritmos de impostos estiverem todos na classe ou no módulo TaxPayer , os conflitos de mesclagem se tornarão prováveis. Conflitos de mesclagem são demorados e propensos a erros. Esse tempo diminui a produtividade da equipe e os erros introduzidos por más combina a credibilidade dos danos com os clientes.

Além disso, o que significa o algoritmo mudar no tempo de execução?

Um algoritmo que muda em tempo de execução é aquele cujo comportamento é determinado pela configuração ou pelo contexto. Uma abordagem if / then no local não habilita isso efetivamente, pois envolve recarregar as classes usadas ativamente existentes. Com o Padrão de Estratégia, os objetos de estratégia que implementam cada algoritmo podem ser construídos em uso. Como resultado, alterações nesses algoritmos (correções ou aprimoramentos) podem ser feitas e recarregadas em tempo de execução. Essa abordagem pode ser usada para permitir disponibilidade contínua e liberações de tempo de inatividade zero.

Alain O'Dea
fonte
1

Não há nada de errado em if/elsesi. Em muitos casos, if/elseé a maneira mais simples e legível de expressar lógica. Portanto, a abordagem que você descreve é ​​perfeitamente válida em muitos casos. (Também é perfeitamente testável, de modo que não é um problema.)

Mas há alguns casos específicos em que um padrão de estratégia pode melhorar a capacidade de manutenção do código geral. Por exemplo:

  • Se os algoritmos de cálculo de impostos específicos podem mudar independentemente um do outro e da lógica principal. Nesse caso, seria bom separá-los em classes distintas, pois as alterações serão localizadas.
  • Se novos algoritmos puderem ser adicionados no futuro, sem que a lógica do núcleo seja alterada.
  • Se a causa da diferença entre os dois algoritmos também afetar outras partes do código. Digamos que você selecione entre os dois algoritmos com base na faixa de renda do contribuinte. Se esse intervalo de renda também fizer com que você selecione ramificações diferentes em outros lugares do código, será mais fácil instanciar uma estratégia correspondente à faixa de renda uma vez e chamar quando necessário, em vez de ter vários ramos if / else espalhados pelo código.

Para que o padrão de estratégia faça sentido, a interface entre a lógica principal e os algoritmos de cálculo de imposto deve ser mais estável do que os componentes individuais. Se for igualmente provável que uma alteração de requisito faça com que a interface mude, o padrão de estratégia pode realmente ser um passivo.

Tudo se resume a se os "algoritmos de cálculo de imposto" podem ser claramente separados da lógica principal que o invoca. Um padrão de estratégia possui alguma sobrecarga em comparação com um if/else, portanto, você terá que decidir caso a caso se o investimento vale a pena.

JacquesB
fonte