Reduzindo de N ifs para nenhum com Strategy em Java

Reduzindo de N ifs para nenhum com Strategy em Java

Estou desenvolvendo um sistema para computar todas as vendas de uma empresa. Atualmente, estou representando cada funcionário da seguinte forma:


public class Funcionario {

private String nome;
private double salario;
private Cargo cargo;

//métodos

}

O atributo cargo é um enum que listará todos os cargos diferentes do sistema:


public enum Cargo {

ATENDENTE, VENDEDOR, GERENTE;

}

Além da representação do funcionário, eu também fiz a representação de uma venda, que possui um funcionário e o valor da venda:


public class Venda {

private final Funcionario funcionario;
private final double valor;

public Venda(Funcionario funcionario, double valor) {
     this.funcionario = funcionario; 
     this.valor = valor; 
    }

}

Porém, cada vez que uma compra for realizada, eu preciso calcular a comissão do funcionário de acordo com o seu cargo, por exemplo, se o funcionário for um atendente, a comissão será de 10% do valor da venda, se for vendedor 15% + 5 reais e gerente 20% + 10 reais. Então vamos criar o método calculaComissao() na classe Venda:


public class Venda {

//atributos e construtor

public void calculaComissao(){ //calcula a comissão }

}

Certo, agora vamos implementar o método para calcular a comissão. Bom, podemos pegar o cargo do funcionário e fazer um if para verificar qual é o cargo dele e então realizar o calculo da sua comissão:


public class Venda {

private final Funcionario funcionario;
private final double valor; //construtor

public void calculaComissao() {

double comissao = 0.0;

Cargo cargo = this.funcionario.getCargo(); 
if (cargo.equals(Cargo.ATENDENTE)) { 
    // comissão de 10% 
    comissao = valor \* 0.1; 
    } else if (cargo.equals(Cargo.VENDEDOR)) { 
        // comissão de 15% + 5 reais 
        comissao = valor \* 0.15 + 5; } else if (cargo.equals(Cargo.GERENTE)) { 
        // comissão de 20% + 10 reais 
        comissao = valor \* 0.20 + 10; 
        }
    }
}

Conseguimos calcular, porém precisamos retornar a comissão da venda, então vamos alterar a assinatura de void para double e então, retornamos a variável comissao:


public class Venda {

//construtor e atributos

public double calculaComissao() {

double comissao = 0.0;

Cargo cargo = this.funcionario.getCargo();
if (cargo.equals(Cargo.ATENDENTE)) { 
    comissao = valor * 0.1; } 
    else if (cargo.equals(Cargo.VENDEDOR)) {
         comissao = valor * 0.15 + 5; } 
            else if (cargo.equals(Cargo.GERENTE)) { 
                comissao = valor * 0.20 + 10; } 
            return comissao;
    }

}

Ótimo, agora precisamos fazer uma simulação para verificar se está funcionando! Então vamos lá:


public class Main {

public static void main(String\[\] args) {

Funcionario atendente = new Funcionario();
tendente.setNome("Maria da Silva"); 
atendente.setSalario(1200.00); 
atendente.setCargo(Cargo.ATENDENTE);

Venda novaVenda = new Venda(atendente, 200.0);

System.out.println("valor da comissão: " + novaVenda.calculaComissao());
}}

Verificando o resultado:

 > valor da comissão: 20.0

Para um atendente o valor da comissão é de 10%, e 10% de 200 é realmente 20! Então está funcionando como esperado! E se tentarmos a mesma venda com um cargo de vendedor?

 > valor da comissão: 35.0

15% de 200 é 30, mais 5 reais fica 35! Por enquanto tudo certo! Vamos verificar o último cargo, ou seja, o de gerente:

 > valor da comissão: 50.0

Excelente! Conforme o esperado, foi impressa uma comissão de 20% que equivale 40, somou 10 e resultou em 50! Aparentemente a minha implementação está impecável! Sem nenhum problema! Só que não! :(

Entendendo o problema dos ifs

Consegue imaginar o que está sendo tão problemático? Vejamos novamente a nossa implementação da calculaComissao():


public class Venda {

//atributos e construtor

public double calculaComissao() {

double comissao = 0.0;

Cargo cargo = this.funcionario.getCargo();
 if (cargo.equals(Cargo.ATENDENTE)) { comissao = valor * 0.1; } 
    else if (cargo.equals(Cargo.VENDEDOR)) { 
        comissao = valor * 0.15 + 5; } 
            else if (cargo.equals(Cargo.GERENTE)) { 
                comissao = valor * 0.20 + 10; }

return comissao; }

}

Veja que, para cada cargo, estamos fazendo um if que identifica qual cargo se refere, e então, calculamos a sua comissão. Vamos supor que a empresa que solicitou uma adição de 3 cargos diferentes, que são: ajudante (8% + 1 real), recepcionista (5%) e diretor (25% + 20 reais):


public enum Cargo {

ATENDENTE, VENDEDOR, GERENTE, AJUDANTE, RECEPCIONISTA, DIRETOR;

}

Como ficaria o nosso método calculaComissao()? Vejamos:


public class Venda {

//métodos, atributos e construtor

public double calculaComissao() {

double comissao = 0.0;

Cargo cargo = this.funcionario.getCargo(); 

if (cargo.equals(Cargo.ATENDENTE)) { 
    comissao = valor * 0.1; } 
else if (cargo.equals(Cargo.VENDEDOR)) { comissao = valor * 0.15 + 5; } 
else if (cargo.equals(Cargo.GERENTE)) { comissao = valor * 0.20 + 10; } 
else if (cargo.equals(Cargo.AJUDANTE)) { comissao = valor * 0.08 + 1; } 
else if (cargo.equals(Cargo.RECEPCIONISTA)) { comissao = valor * 0.05; } 
else if (cargo.equals(Cargo.DIRETOR)) { comissao = valor * 0.25 + 20; }

return comissao; }

}

Observe que o nosso método calculaComissao(), atualmente, tende a crescer e muito! Com certeza essa é uma má prática, pois se um dia adicionarmos um novo cargo e esquecermos de adicionar nesse conjunto gigantesco de ifs e elses, o nosso sistema apresentará um bug facilmente...

Além disso, e se quisermos reutilizar esse método em uma outra classe qualquer? Teremos que fazer um copy e paste de todas essas condições e, se um dia mudar a regra de negócio e tiver um reajuste na comissão? Teremos que mudar em todos os pontos do nosso sistema que está utilizando esse tipo de implementação...

Veja quanta complicação! Com certeza é uma solução trabalhosa e não tão inteligente! Sabendo desses problemas, como poderíamos resolver isso?

Banner promocional da Alura, com um design futurista em tons de azul, apresentando dois blocos de texto, no qual o bloco esquerdo tem os dizeres:

Separando as responsabilidades

Que tal isolarmos a responsabilidade de calcular a comissão para cada cargo? Mas como assim isolar a responsabilidade? Exatamente isso, fazer com que o próprio cargo saiba calcular a sua comissão, ou seja, podemos fazer com o que cada enum já implemente um método calculaComissao()!


public enum Cargo {

ATENDENTE { public double calculaComissao(double valor){ 
    return valor * 0.1; } }, VENDEDOR,GERENTE,AJUDANTE,RECEPCIONISTA,DIRETOR;

}

Repare que, ainda existe um grande problema, pois da forma que está atualmente, o nosso enum compila nesse instante, porém quebra todos os pontos do sistema que o chama, pois não é garantido que todos os valores do enum tenham esse método!

Precisamos, de alguma forma, garantir que todos os cargos tenham um método calculaComissao()!

Garantido a chamada de métodos por meio de interface

E agora? Como podemos fazer isso? Da mesma forma que em classes, nós podemos fazer com que o enum implemente uma interface que obrigue a implementação do método calculaComissao()! Então vamos criar essa interface:


public interface Comissao {

public double calculaComissao(double valor); }

Agora que temos a interface Comissao, basta implementá-la no enum e sobrescrever o método para todos os cargos:


public enum Cargo implements Comissao {

ATENDENTE { @Override public double calculaComissao(double valor) { 
    return valor * 0.1; } }, 

VENDEDOR { @Override public double calculaComissao(double valor) { 
    return valor * 0.15 + 5; } },

GERENTE { @Override public double calculaComissao(double valor) { 
    return valor * 0.20 + 10; } },

AJUDANTE { @Override public double calculaComissao(double valor) { 
    return valor * 0.08 + 1; } }, 

RECEPCIONISTA { @Override public double calculaComissao(double valor) { 
    return valor * 0.05; } }, 

DIRETOR { @Override public double calculaComissao(double valor) { 
    return valor * 0.25 + 20; } };

}

Certo, definimos o método calculaComissao() para cada cargo, e como ficaria o método calculaComissao() da classe Venda? Vejamos o que muda:


private final Funcionario funcionario; private final double valor;

public Venda(Funcionario funcionario, double valor) { 
    this.funcionario = funcionario; 
    this.valor = valor; }

public double calculaComissao() {

double comissao = 0.0;

Cargo cargo = this.funcionario.getCargo(); 
comissao = cargo.calculaComissao(valor);

return comissao;

}

}

Cade aqueles ifs e elses??? Será que ainda funciona como antes? Vamos testar novamente, para todos os cargos para uma compra no valor de 200:

Atendente (10%):

 > valor da comissão: 20.0

Vendedor (15% + reais):

 > valor da comissão: 35.0

Gerente (20% + 10 reais):

 > valor da comissão: 50.0

Ajudante (8% + 1 real):

 > valor da comissão: 17.0

Recepcionista (5%):

 > valor da comissão: 10.0

Diretor (25% + 20 reais):

 > valor da comissão: 70.0

Excelente! Está funcionando conforme o esperado, porém agora, nós podemos calcular a comissão de um funcionário chamando apenas 1 método e em qualquer lugar!

E se agora precisarmos fazer aquele reajuste?

Basta alterar apenas no enum e funcionará da mesma forma para todas as classes que chamarem o método calculaComissao()!

Todo conceito por de trás da nossa implementação

A solução que utilizamos não é uma invenção minha, isso é um Design Pattern (na tradução, padrão de projeto) chamado Strategy que, por meio de polimorfismo, nos permite adicionar/alterar implementações de um método em comum, deixando-as encapsuladas, sem que impacte as chamadas realizadas pelo cliente.

Em outras palavras, podemos fazer diversas variações de um mesmo método em comum (por exemplo, o calculaComissao()) que precise de um comportamento diferente para cada situação, nesse caso, o calculo de comissão para cada cargo diferente!

A implementação demonstrada no artigo é uma das possíveis implementações do Strategy, ou seja, existem diversas outras!

Isso significa que é muito mais importante entender o conceito utilizado para resolver o problema com o Strategy do que apenas a implementação, ou melhor dizendo, identificar uma situação que apresenta muitas variações para uma determinada funcionalidade que por sua vez, são condicionais, como foi o caso do calculo de comissão ser diferente para cada cargo diferente, e então aplicar o Strategy!

E aí, o que achou do Strategy? Nunca havia pensado que os if algum dia poderiam sumir?

Quer aprender mais sobre Design Pattern? Na Alura, temos o curso de Design Pattern em Java que demonstra tanto o Strategy como outros Design Patterns, explicando como cada um funciona e aonde podemos aplicar cada um deles!

Alex Felipe
Alex Felipe

Alex é instrutor e desenvolvedor e possui experiência em Java, Kotlin, Android. Atualmente cria conteúdo no canal https://www.youtube.com/@AlexFelipeDev.

Veja outros artigos sobre Programação