Reduzindo de N ifs para nenhum com Strategy em Java

(Última atualização em: 29 de maio de 2017)

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();
          atendente.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?

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:

 
public class Venda {

     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 post é 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? Compartilhe conosco suas impressões nos comentários 😉

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!

Content Editor at Alura and Software Developer

Próximo ArtigoDepoimento do aluno: Wanderson Macêdo