DEV Community

mariasantosdev
mariasantosdev

Posted on

Padrão de projeto - Strategy

Problema
Imagine o cenário onde a equipe de desenvolvimento decide criar um sistema responsável por fazer o cálculo do imposto de renda.

Para calcular o imposto de renda é necessário verificar o salário do funcionário para ver em qual faixa do imposto de renda ele se encaixa e depois disso aplicar o desconto. Sendo que o desconto é o salário multiplicado pelo percentual da alíquota e subtraído pela parcela a deduzir.

Para compreender um pouco mais quais dos dados são relevantes para aplicar ou não o desconto segue a tabela do imposto de renda:

Image description

Fonte: Receita federal

Olhando para essa tabela já começamos a imaginar estruturas de decisões e desenharmos na nossa mente algo semelhante ao seguinte código:

  Scanner teclado = new Scanner(System.in).useLocale(Locale.US);
        double salario = teclado.nextDouble();

        if (salario <= 1903.98) {
            System.out.println("nao ha deducao fiscal");

        } else if (salario >= 1903.99 && salario <= 2826.65) {
            System.out.println("A deducao fiscal e de 7,5%");
            System.out.println("Voce deve deduzir o valor de " + ((salario * 0.075) - 142.80) + " reais");

        } else if (salario >= 2826.66 && salario <= 3751.05) {
            System.out.println("A deducao fiscal e de 15%");
            System.out.println("Voce deve deduzir o valor de " + ((salario * 0.15) - 354.80) + " reais");

        } else if (salario >= 3751.06 && salario <= 4664.68) {
            System.out.println("A deducao fiscal e de 22,5%");
            System.out.println("Voce deve deduzir o valor de " + ((salario * 0.225) - 636.13) + " reais");
        } else if (salario >= 4664.68) {
            System.out.println("A deducao fiscal e de 27,5%");
            System.out.println("Voce deve deduzir o valor de " + ((salario * 0.275) - 869.36) + " reais");
        }
Enter fullscreen mode Exit fullscreen mode

Essa é uma abordagem para chegar ao resultado, porém há alguns problemas nesse código, o primeiro é estarmos usando o tipo double para uma variável que trabalhará com valores monetários e o segundo é a probabilidade desse código crescer mais e termos uma estrutura de decisão muito grande.

Mas qual o malefício com estruturas de decisões que tendem a crescer muito?
Um malefício dessas estruturas ficarem muito grandes é que caso precisem implementar uma nova funcionalidade, as chances de quebrar as funcionalidades existentes é grande. Isso acontece devido ao alto acoplamento.

Como podemos alterar esse código?
Ao invés de usarmos estruturas de decisões que tendem a ficar cada vez maiores, podemos optar usar uma ferramenta muito poderosa que é um dos pilares da programação orientada a objetos, o polimorfismo.

Para refatorar esse código removendo as estruturas de decisões o primeiro passo seria criar um método que vai ser o responsável pelo cálculo em si e outro método booleano responsável por ver se a regra se aplica para aquele salário.

public interface CalculadoraImpostoDeRenda {

    boolean deveAplicarPara(BigDecimal salario);

    BigDecimal efetuarCalculo(BigDecimal salario);
}
Enter fullscreen mode Exit fullscreen mode

Nesse exemplo preferi criar uma interface com esses métodos, pois o nosso próximo passo será criar diversas classes, cada uma representando um "if" que tinhamos e elas implementarão a interface CalculadoraImpostoDeRenda.

A seguir o exemplo de duas classes:

public class ImpostoDeRendaIsento implements CalculadoraImpostoDeRenda {

    private final static BigDecimal VALOR_MAXIMO = new BigDecimal("1903.98");

    @Override
    public boolean deveAplicarPara(BigDecimal salario) {
        return salario.compareTo(VALOR_MAXIMO) <= 0;
    }

    @Override
    public BigDecimal efetuarCalculo(BigDecimal salario) {
        if(!deveAplicarPara(salario)) throw new RuntimeException("Salario não se aplica para essa regra");
        return ZERO;
    }
}
Enter fullscreen mode Exit fullscreen mode
public class ImpostoDeRendaMedioBaixo implements CalculadoraImpostoDeRenda {

    private final static BigDecimal VALOR_MINIMO = new BigDecimal("1903.99");
    private final static BigDecimal VALOR_MAXIMO = new BigDecimal("2826.65");

    @Override
    public boolean deveAplicarPara(BigDecimal salario) {
        return salario.compareTo(VALOR_MINIMO) >= 0 && salario.compareTo(VALOR_MAXIMO) <= 0;
    }

    @Override
    public BigDecimal efetuarCalculo(BigDecimal salario) {
        if(!deveAplicarPara(salario)) throw new RuntimeException("Salario não se aplica para essa regra");
        return (salario.multiply(new BigDecimal("0.075"))
                .subtract(new BigDecimal("142.80"))
                .setScale(2, RoundingMode.HALF_UP));
    }
}
Enter fullscreen mode Exit fullscreen mode

Nessa refatoração já estamos usando o BigDecimal para representar o salário por conta da falta de precisão dos pontos flutuantes(mais sobre esse assunto nesse link aqui), e por isso estamos fazendo comparações utilizando compareTo.

Cada classe verifica se o salário se aplica para aquela regra e depois efetua de fato o cálculo de acordo com a alíquota.

Temos que fazer isso para os cinco casos que existem na tabela do imposto de renda.

Depois disso, fazemos a criação de um enum que terá esses cinco casos e terá um método (calcularImpostoDeRenda) que será o responsável por de fato fazer o cálculo correto de acordo com o salário.

public enum TipoDoImpostoDeRenda {

    ISENTO(new ImpostoDeRendaIsento()),
    MEDIO_BAIXO(new ImpostoDeRendaMedioBaixo()),
    MEDIO_ALTO(new ImpostoDeRendaMedioAlto()),
    ALTO(new ImpostoDeRendaAlto()),
    TETO(new ImpostoDeRendaTeto());

    private final CalculadoraImpostoDeRenda calculadoraImpostoDeRenda;

    TipoDoImpostoDeRenda(CalculadoraImpostoDeRenda calculadoraImpostoDeRenda) {
        this.calculadoraImpostoDeRenda = calculadoraImpostoDeRenda;
    }

    public static BigDecimal calcularImpostoDeRenda(BigDecimal salario) {
        return Arrays.stream(TipoDoImpostoDeRenda.values())
                .filter(t -> t.deveAplicarPara(salario))
                .findFirst()
                .map(i -> i.efetuarCalculo(salario))
                .orElseThrow(() -> new RuntimeException("Tipo de imposto de renda não encontrado"));
    }
    private boolean deveAplicarPara(BigDecimal salario) {
        return calculadoraImpostoDeRenda.deveAplicarPara(salario);
    }

    private BigDecimal efetuarCalculo(BigDecimal salario) {
        return calculadoraImpostoDeRenda.efetuarCalculo(salario);
    }
}
Enter fullscreen mode Exit fullscreen mode

Perceba que a partir desse momento passamos a usar a interface para fazer essa lógica, ou seja, caso outro filtro do imposto venha a existir nós só vamos adicionar outra classe e mais um valor nesse enum.

Agora é só fazer a chamada desse método, temos a classe PessoaFisica que contém o atributo salário:

public class PessoaFisica {
    private BigDecimal salario;

    public PessoaFisica(String salario) {
        this.salario = new BigDecimal(salario);
    }

    public BigDecimal calcularSalarioLiquido() {
        return TipoDoImpostoDeRenda.calcularImpostoDeRenda(this.salario);
    }
}
Enter fullscreen mode Exit fullscreen mode

Depois disso podemos criar uma main para testar se o nosso código está funcionando:

public class Teste {
    public static void main(String[] args) {
        PessoaFisica pessoaFisica = new PessoaFisica("2423.00");
        BigDecimal salarioLiquido = pessoaFisica.calcularSalarioLiquido();
        System.out.println(salarioLiquido);
    }
}

Enter fullscreen mode Exit fullscreen mode

Dessa forma refatoramos o código utilizando o padrão de projeto strategy!

Oque é o padrão de projeto strategy?
A refatoração que acabamos de fazer foi feita com base em um padrão de projeto chamado strategy.

O strategy é um padrão de projetos que deve ser utilizado em estruturas de decisões como if e switch que tendem a crescer bastante.

A solução proposta pelo padrão strategy é encapsular o algoritmo responsável por variar e isso pode ser feito de algumas maneiras (no nosso exemplo foi na interface CalculadoraImpostoDeRenda).

O padrão strategy é indicado quando temos um parâmetro e sabemos que aquela regra será aplicada naquele determinado parâmetro. No exemplo apresentado sabiamos exatamente quais eram os diferentes tipo de impostos que tinhamos.

Talvez, nesse caso, específico, nem precisamos do Strategy. É possível pensar em uma regra geral de cálculo, deixando-a no enum e evitando criar as classes: ImpostoDeRendaIsento, ImpostoDeRendaMedioBaixo, ImpostoDeRendaMedioAlto, ImpostoDeRendaAlto, ImpostoDeRendaTeto e até mesmo a interface CalculadoraImpostoDeRenda.

Dessa forma:

enum TipoDoImpostoDeRenda {

    ISENTO(new BigDecimal("-1"), new BigDecimal("1903.98"), BigDecimal.ZERO, BigDecimal.ZERO),
    MEDIO_BAIXO(new BigDecimal("1903.99"), new BigDecimal("2826.65"), new BigDecimal("0.075"), new BigDecimal("142.80")),
    MEDIO_ALTO(new BigDecimal("2826.66"), new BigDecimal("3751.05"), new BigDecimal("0.150"), new BigDecimal("354.80")),
    ALTO(new BigDecimal("3751.06"), new BigDecimal("4664.68"), new BigDecimal("0.225"), new BigDecimal("636.13")),
    TETO(new BigDecimal("4664.69"), new BigDecimal(Integer.MAX_VALUE), new BigDecimal("0.275"), new BigDecimal("869.36"));

    private final BigDecimal valorMinimo;
    private final BigDecimal valorMaximo;
    private final BigDecimal aliquota;
    private final BigDecimal parcelaADeduzir;

    TipoDoImpostoDeRenda(BigDecimal valorMinimo, BigDecimal valorMaximo, BigDecimal aliquota, BigDecimal parcelaADeduzir) {
      this.valorMinimo = valorMinimo;
      this.valorMaximo = valorMaximo;
      this.aliquota = aliquota;
      this.parcelaADeduzir = parcelaADeduzir;
    }

    public static BigDecimal calcularImpostoDeRenda(BigDecimal salario) {
        return Arrays.stream(TipoDoImpostoDeRenda.values())
                .filter(t -> t.deveAplicarPara(salario))
                .findFirst()
                .map(i -> i.efetuarCalculo(salario))
                .orElseThrow(() -> new RuntimeException("Tipo de imposto de renda não encontrado"));
    }

    private boolean aplica(BigDecimal salario) {
      return salario.compareTo(valorMinimo) >= 0 && salario.compareTo(valorMaximo) <= 0;
    }

    private BigDecimal calcula(BigDecimal salario) {
      return (salario.multiply(aliquota)
                .subtract(parcelaADeduzir)
                .setScale(2, RoundingMode.HALF_UP));
    }
}
Enter fullscreen mode Exit fullscreen mode

Dessa forma manteríamos apenas as classes PessoaFisica e Teste, deixando o enum um pouco mais complexo de se entender, no entanto, removendo várias classes que usamos para nos auxiliar nesse padrão.

Vantagens do strategy

  • Um código bem mais limpo, pois você isola os detalhes da implementação;

  • Um algoritmo que pode ser alterado mais facilmente, mexendo somente na classe responsável;

  • Princípio SOLID Aberto/fechado, pois se insere novas classes sem mudar o contexto.

Desvantagens do strategy

  • Aumento na complexidade do código, pois precisa ser criada a instância de diferentes classes. Devido a isso é sempre importante ter critério na utilização do pattern.

Aqui está o link do código completo do exemplo:
https://github.com/mariasantosdev/calculo-imposto-de-renda-strategy

Se quiser ir além e ver também outro exemplo onde o strategy é aplicado em um enum:
https://github.com/mariasantosdev/jogo-dinossauro

Materiais utilizados de apoio para escrever o artigo
https://www.casadocodigo.com.br/products/livro-design-patterns
https://refactoring.guru/pt-br/design-patterns/strategy

Top comments (1)

Collapse
 
henriqueln7 profile image
Henrique Lopes Nóbrega

Muito legal, Madu!
Gosto bastante da estratégia que você mostrou de utilizar o Strategy aliado com o Enum. Em minha opinião, o Enum é ótimo para aplicar esse design pattern. 👏👏👏