Como não aprender orientação a objetos: o excesso de ifs

Aglomerados de ifs aparecem com frequência, e chegam até a ter um aspecto engraçado. Em alguns casos poder dar a impressão de que estamos usando orientação a objetos, já que cada cláusula costuma envolver a invocação de um método, dependendo do tipo do objeto. Infelizmente, essa sensação é falsa, e chegou até a gerar o conhecido movimento anti if campaign na internet.

O exemplo a seguir mostra uma sequência de ifs que indicam condições distintas de execução:

public double calculaBonus(Funcionario f) {
 if (f.getCargo().equals("gerente")) {
   return f.getVendasDoMes() * 0.05 + getSalario() * 0.1;
 } else if (f.getCargo().equals("diretor")) {
   return f.getVendasDoMes() * 0.05 + getSalario() * 0.2 + (Today.isDecember() ? getSalario() : 0);
 } else {
   return f.getVendasDoMes() * 0.01;
 }
}

Esse tipo de condicional pode ser retrabalhado em objetos (ou funções dependendo da linguagem) que definam o comportamento a ser executado em cada caso. Herança e poliformismo podem (e na maioriam dos casos, devem) ser usados de maneira controlada para alterar um comportamento:

class Funcionario {
 // outros atributos e metodos aqui
 public double getBonus() {
   return vendasDoMes * 0.01;
 }
}
class Gerente extends Funcionario {
 // outros atributos e metodos aqui
 public double getBonus() {
   return vendasDoMes * 0.05 + getSalario() * 0.1;
 }
}

Nesse caso, os responsáveis pelo comportamento são encontrados de acordo com uma condição: o tipo que o objeto representa, sendo que cada subtipo pode redefinir o comportamento do método. Não haverá necessidade de ifs, já que essa descoberta de qual método executar é feita pela máquina virtual: funcionario.getBonus().

Esses ifs não costumam aparecer sozinhos. Assim como o comportamento
do bonus, surgem em breve outros comportamentos com os mesmos ifs em outras partes do sistema:

public double liberaVerba(Funcionario f, Produto produto) {
 if (f.getCargo().equals("gerente")) {
  return produto.getValor() < 5000 || produto.getTipo().equals(Tipo.URGENTE);
 } else if (f.getCargo().equals("diretor")) {
  return produto.getValor() < 10000;
 } else {
  return produto.getValor() < 1000 ||
produto.getTipo().equals(Tipo.USO_DIARIO);
 }
}

Esse tipo de código fica então muito instável: uma mudança de
comportamento deve ser alterada em diversos lugares distintos e, pior
ainda, é muito fácil esquecer um deles ao criar um novo comportamento:
alguns dos pontos de multiplos ifs são esquecidos. Ao escrever o mesmo código pela segunda vez, seguimos a prática sugerida no Pragmatic Programmers, de não nos repetir (DRY).

Mas o uso da herança é delicado, e o desenvolvedor deve estar ciente de que ela pode trazer um acoplamento indesejado e suas alternativas. O uso de interfaces se encaixaria aqui com perfeição.

Outros tipos de condições podem determinar qual ação deve ser tomada, como por exemplo o valor de um parâmetro, resultando em uma abordagem que utiliza um mapa. Note como, nesse caso, novamente, nenhum switch ou sequências de ifs precisam ser feitos: ifs são substituídos por polimorfismo:

private Map<String, Aplicador> taxas = new HashMap<String, Aplicador>();

public void processa(String taxa, double juros) {
 impostosRecolhidos += taxas.get(taxa).aplicaComJuros(juros);
}

De todas as variações, a solução do registro (em geral um Map) e a chave é a abordagem mais simples e capaz de separar responsabilidades distintas. Uma outra variação, que remove a necessidade de registrar os itens no mapa, mas abusa de reflection, muitos casos desnecessariamente, é a seguinte:

interface AplicadorDeTaxa {
   double aplicaComJuros(double valor);
 }
}

public void processa(String taxa, double juros) {
 Object instancia = Class.forName("br.com.caelum.taxas." + taxa).newInstance();
 AplicadorDeTaxa aplicador = (AplicadorDeTaxa) instancia;
 impostosRecolhidos += aplicador.aplicaComJuros(juros);
}

A invocação a Class.forName("br.com.caelum.taxas." + taxa).newInstance(); pode ainda ser encapsulada em uma Factory, que em vez de buscar por um nome de classe, consultaria anotações ou um arquivo de configuração.

Esses problemas com o if surgem também em outros paradigmas. Em linguagens funcionais é possível atingir o mesmo resultado usando lambdas, ou ainda em procedurais é possível passar ponteiros de funções com abstract data types.

Além dos casos em que ifs e condicionais podem ser trocados pelo bom uso de polimorfismo, podemos seguir as boas práticas e evitar condicionais complicados e muito aninhados.

47 Comentários

  1. Henrique Honjoya 12/04/2011 at 10:50 #

    Muito boa explicação .. parabéns

  2. Victor Magno 12/04/2011 at 11:23 #

    Excelente post! É imprescindivel ressaltar a importância da orientação a objetos para a qualidade de qualquer projeto.

  3. thiagocifani 12/04/2011 at 12:47 #

    Belo Post guilherme! muito bom!

  4. Marcelo 12/04/2011 at 13:10 #

    Poderia postar um exemplo para um caso de validação sem uso abusivo de IFs… exemplo se precisar validar uma senha onde

    senha.length() > 4;
    senha.length() < 10;
    senha.hasSpecialCharacter();
    !senha.equals(login);

    Obrigado

  5. leandro 12/04/2011 at 13:29 #

    E olha às vezes a gente faz isso sem ver.. ;\ ótimo post.

  6. Wellington Vieira 12/04/2011 at 13:33 #

    Gostei…. muito bom o post.

  7. Rafael Felix 12/04/2011 at 14:09 #

    Gulherme,

    esse seu segundo exemplo esse newInstance me fez lembrar o pattern do Daniel Destro no GUJ http://www.guj.com.br/java/234881-ondemand—novo-design-pattern

    Muito facil quebrar esse codigo não? Já usei essa abordagem algumas vezes, e confesso que não acho legal.

    abraços

  8. Paulo Silveira 12/04/2011 at 14:23 #

    @Rafael sim, esse command-pattern baseado em String é sem duvida fraco, ainda mais se ele fosse receber Object[] como argumento, perdendo toda e qualquer tipagem.

    Por isso o ideial é, quando necessario, isola-lo numa factory, que pode instanciar uma classe e verificar permissoes atraves de uma enum, anotacoes, ou arquivo de configuracao. E o command, salvo rarissimas excecoes, deve possuir uma relacoa. Isso é, nao trabalhar em cima de Object para ca e para la.

    E nomes mais significativos sao mandatorios tambem. Nesse caso do post, creio que AplicacaoDeTaxa em vez de Acao e aplicaComJuro em vez de executa.

    ps: ate mudei o nome da interface e do metodo, valeu Rafael, veja se esta melhor.

  9. Anderson Sanches 12/04/2011 at 15:16 #

    Os exemplos de nomes dos métodos, no blog, sempre me causam estranheza, por não estarem no infinitivo.

  10. Paulo Silveira 12/04/2011 at 15:25 #

    @Anderson, boa questao… eu prefiro muitas vezes nao deixar no infinitivo, como hasElement, contains, etc… ficaria bem estranho ser no infinitivo. Mesmo quando algo sera executado, prefiro o presente ou o imperativo: aplicaTaxa apliqueTaxa, mas aplicarTaxa também é possível. Importante é seguir um caminho.

  11. Felipe 12/04/2011 at 15:31 #

    Gui, dá uma revisada nos exemplos cara, pode gerar confusões, mas excelente o post.

  12. Anderson Sanches 12/04/2011 at 18:41 #

    @PauloSilveira Quando o método faz uma pergunta ao objeto, como nos exemplos que você citou, certamente faz mais sentido flexionar o verbo. Nos outros casos eu evito justamente por causa da confusão (era aplicaTaxa ou apliqueTaxa?). No caso do imperativo, temos a forma negativa, que pode também causar alguma dúvida. Mas como você disse, o importante é escolher uma forma e seguí-la.

  13. Dalton Barreto 12/04/2011 at 23:14 #

    @Marcelo, nesse caso você poderia, por exemplo, definir uma intrface “ValidadorSenha”:

    interface ValidadorSenha {
      public Boolean senhaBoa(String login, String senha);
    }
    

    Depois para cada uma das validaçõs pode pode ter uma implementação separada, assim você poderia ter, por exemplo: ValidadorTamanho, ValidadorCharEspecial, ValidadorX,….

    E então no código principal você faria algo do tipo:

    public validaSenha(String login, String senha){
       List validadores = retornaValidadoresSenha();
       for (ValidadorSenha v: validadores){
          if (!v.senhaBoa(login, senha)){
            // Trata o porque da senha "ruim"
          }
       }
    }
    

    Assim, a cada “novo validador” o algoritmo não muda, basta que o método (aqui fake) retornarValidadoresSenha() saiba pegar/instanciar todas as classes que implementam a interface em questão, isso pode ser feito como o @PauloSilveira já disse, via anotaçao, config-file ou o método que achar mais adequado.

    Espero ter ajudado.

    ps. Desculpem se a indentação estiver incorreta.

  14. Guilherme Silveira 13/04/2011 at 03:16 #

    Opa @Rafael, na prática, usar um Map resolve facinho o problema só com a necessidade de verificar a existência do handler necessário no mapa. É a abordagem que eu, pessoalmente, mais gosto, para fazer o lookup de um handler, como nesse caso.

    Opa @Anderson, acho que o ideal é encontrar para cada caso se você deseja conjugá-lo ou não. Eu tenho criado cada vez mais modelos e conjugado, mas é um gosto pessoal; em um projeto em grupo, é importante que o o grupo todo fique confortável com a maneira de definir o vocabulário – seja ele qual for, como você e o Paulo disseram.

    Abraço!

  15. Rafael Felix 13/04/2011 at 08:26 #

    @Guilherme o uso do map eu também prefiro, fica muito mais simples, além do que eu poderia ter implementações em outro pacote, caso se faça necessário.

    Só vejo um problema nessa abordagem, quando se tiver muitos handlers, uns 200 por exemplo, o código pra preencher esse map fica muito ruim. Claro que com ifs ficaria pior, seria uma cadeira de ifs complexa e de difícil manutenção. Nesse caso não sei se existe uma outra abordagem que posso melhorar o codigo.

    Abraços

  16. Dev 13/04/2011 at 14:30 #

    Neste método tem um monte de IFs, tá errado?

    /*
    *Remove as máscaras aplicadas no Input do Formulário.
    */
    public String removeMascara(String campo){
            String novoCampo = "";        
            char [] arrayChar = new char[campo.length()];
            int aux = 0;
    
            if (campo.equals("") | campo == null){
                throw new IllegalArgumentException("Campo Nulo");            
            }else{            
                for (int i = 0; i &lt; campo.length(); i++){
                    if (campo.charAt(i) == &#039;0&#039; | campo.charAt(i) == &#039;1&#039; | campo.charAt(i) == &#039;2&#039; |
                        campo.charAt(i) == &#039;3&#039; | campo.charAt(i) == &#039;4&#039; | campo.charAt(i) == &#039;5&#039; |
                        campo.charAt(i) == &#039;6&#039; | campo.charAt(i) == &#039;7&#039; | campo.charAt(i) == &#039;8&#039; |
                        campo.charAt(i) == &#039;9&#039;){
                        arrayChar[aux] = campo.charAt(i);
                        aux++;
                    }
                }            
                for (int i = 0; i &lt; aux; i++){
                    novoCampo += arrayChar[i];
                }
                return novoCampo;
            }
        }
    
  17. Ernesto 13/04/2011 at 16:29 #

    alguma sugestão para o uso exagerado de instaceof’s? Me deparei com o seguinte problema uma vez: possuía uma árvore onde todos os nós eram de uma interface Node. Precisava executar uma ação qualquer para cada nó dependendo do seu tipo mais específico (classe) e não do mais abstrato (interface). Não encontrei outra maneira de resolver o problema a não ser usar uma sequência horrível de if(obj instanceof classe) { facaAlgo(); } else if…

  18. Paulo Silveira 13/04/2011 at 16:51 #

    @dev eu daria um extracr method no for interno, para diminuir a quantidade de blocos aninhados, mas nao é desse tipo de if que o post se trata, e sim aquele que pode ser eliminado por tipagens que ja existem.

    @Ernesto esse é um caso excelente para fazer varias mudancas e ver o que acha melhor. em vez do instanceOf para chamar o facaAlgo(), por que nao definir, dentro da interface Node, o metodo facaAlgo, que faz coisas diferentes de acordo com o tipo real do Nó?

  19. Guilherme Silveira 13/04/2011 at 20:03 #

    Oi @Rafael, você está certo. Se o numero de handlers a serem registrados for muito grande, ai passa-se a usar algum tipo de auto loading deles (busca por pacote, uma dsl simples de registro, qualquer coisa que facilite).

    Mas sempre lembrando de refatorar passo a passo, do mais simples (usa menos recursos complexos) pro mais complexo (usa mais meta programação/reflection, mais generico etc)

    Abraço

  20. Jeferson 14/04/2011 at 00:32 #

    sim; mas há alguns casos como alguns problemas com arrays e matrizes que o uso de ‘if’s’ é necessario(pelomenos eu ainda nao achei solução melhor), mas em outros casos, eu ainda fazia o mesmo encadeamento de if’s… obrigado, esse post abriu meus olhos;

  21. Ernesto 14/04/2011 at 16:03 #

    @Paulo esqueci de adicionar um detalhe, a árvore gerada e a interface Node são de uma API de terceiros, o código é até aberto, mas não queria alterar nada lá.

  22. Paulo Silveira 14/04/2011 at 16:08 #

    Oi Ernesto.

    Ai ficou complicado mesmo. Se comecar a ficar complicado e voce achar que vale a pena, eu criaria um wrapper (ou uma interface decorator com mais metodos) em cima da interface Node dessa api de terceiros. Nessa interface MyNode, colocaria um getNode que devolve o Node da api de tericeiros, assim como o metodo facaAlgo!

  23. Ricardo 14/04/2011 at 19:06 #

    Seila, esse exemplo com reflection é a típica coisa que peço para ninguém fazer aqui, prefiro mil vezes ter 3 if’s a ter algo assim no meu sistema.

    Nessa de não querer usar if, um programador aqui resolveu usar class.getConstructors(), assim acha o construtor certo de forma dinâmica, tudo “bonitão”.

    Outro foi lá, viu um construtor com X parâmetros, checou quem chamava (ninguém), apagou. Isso foi dar um runtime exception lá para frente, e ai descobrimos a gambiarra com getConstructors.

    Ok, isso não ocorreria se tivessem testes testando aquela chamada, e também não ocorreria no exemplo de vocês, pois poderia checar quem chama o método, mas mesmo assim acho uma complexidade desnecessária, seria difícil explicar para quem não manja de reflection o código, e criaria um risco muito maior que se eu tivesse os 3 if’s iniciais.

    Enfim, não gosto de reflection, para mim a solução ideal ainda está no polimorfismo.

  24. Paulo Silveira 14/04/2011 at 19:18 #

    Oi Ricardo

    Perfeita observacao. polimorfismo e o bom uso de interfaces deve ser sempre priorizado em relacao a reflection, que traz uma complexidade.

    Em especial a ideia é que os ifs, ou o reflection, devam ficar dentro de um unico factory, para que essa verificacao pelo menos fique num unico lugar.

  25. Guilherme Silveira 14/04/2011 at 21:31 #

    Oi Ricardo,

    Acho que a resposta do map que está logo em cima em outro comentário é a que resolve o problema que você não gosta.
    Quanto menos reflection, melhor. Mas switchs costumam aparecer em grupos. O foco do post é o polimorfismo, e não reflection.

    Abraço

  26. Fernando Boaglio 18/04/2011 at 09:18 #

    @Ernesto @Paulo acho que um exemplo mais elaborado do problema do Ernesto merecia um novo post aqui , que tal ? =)

  27. Rodrigo 20/04/2011 at 10:24 #

    Não entendi muito bem a solução de Map para o problema que o @Rafael levantou… Como funcionaria esta Map?

  28. Carlos 27/04/2011 at 11:39 #

    Tb não entendi a solução do Map…rs. Anyone?

    O post está muito bom. Parabéns!

  29. Paulo R. A. Sales 27/04/2011 at 17:31 #

    Olá Guilherme,

    No exemplo usando reflection você poderia ter optado pelo pattern Strategy, usando interface.

    Fica a dica! Mas um belo post, sucinto e bem escrito! Parabéns!

  30. Bruno Frank 28/04/2011 at 13:53 #

    Ficou pior do que os ifs, agora no lugar de 3 ifs eu vou usar uma factory interfaces e reflection pra não usar 3 ifs vou escrever no mínio 6 units com dezenas linhas de código.

  31. Guilherme Silveira 28/04/2011 at 14:39 #

    Oi Bruno,

    Você está certo. Se tudo o que você tem são três ifs que verificam esse padrão no sistema inteiro não terá muitos problemas com o passar do tempo. Se cada uma das linhas do if tem uma linha de código, melhor ainda.

    O exemplo dado é um simples onde os grandes prejuízos ainda não surgem. Conjuntos de ifs costumam aparecer de maneira similar em diferentes partes do sistema. Assim como nesse exemplo o bonus dependia do cargo, outra parte do sistema calculará outra tarefa de acordo com o cargo, outra parte, uma terceira função que depende do cargo. Esses conjuntos de ifs não costumam aparecer de um só e, nesse momento, fica claro que o comportamento de um “gerente” (por exemplo) ficou espalhado em varios pontos atraves de diversos ifs, ao inves de estar em um ponto.

    Toda decisão de design vai do tamanho do problema. Se ele tem 4 linhas, ainda está ok. Assim que passa a ter 20 e os padrões de ifs aparecerem em mais de um lugar, é hora de mudar.

    Abraço!

  32. Francismar 28/04/2011 at 16:22 #

    Parabéns pelo post

  33. Carlos 28/04/2011 at 17:20 #

    Realmente, concordo com Bruno Frank.

    Um coisa simples que se faz com IF, criar dezenas de classes e metodos, apenas para uma simples coisa, pode até ser mais orientado a objetos desta forma, entretanto hoje em dia vc precisa de velocidade e simplicidade para desenvolver qualquer que seja o programa.

  34. Guilherme Silveira 03/05/2011 at 11:19 #

    Oi pessoal,

    Atualizei o post com o exemplo do Map para registro e também uma indicação mais forte do abuso dos ifs (agora ao inves de somente 3 ifs, temos 3 ifs replicados no sistema, padrão comum que aparece), obrigado pelas sugestões.

    Abraço

  35. walmir Santos 09/05/2011 at 18:54 #

    Ola pessoal
    gostaria de aprender programar em java pois tenhos diculdade nos laços e alguns comando para enserir elementos

    Hum forte

    Abraç…

  36. Bruno 18/05/2011 at 22:51 #

    Já dizia Linus Torvalds: “se você está utilizando três níveis encadeados ou mais, considere a possibilidade de revisar a lógica de seu programa”.

  37. Fidelis 26/05/2011 at 17:03 #

    Cara, muito bacana esse post, principalmente pelo Duck Typing que você utilizou nas classes Funcionario e Gerente, dessa forma o método fica responsavel pelo execução dos calculos, sem a necessidade de anhinhamento de if’s.

    Depois desse post, você me abriu os olhos em algumas coisas, valeu man!

  38. Igor K. Shiohara 01/06/2011 at 10:08 #

    É importante ressaltar que o uso correto da OO previne menos manutenção mais pra frente e que fica apto a usar este tipo de interfaces para outras implementações, evitando duplicidade e acoplamento de código.

  39. Eraldo 02/06/2011 at 23:00 #

    Muito bom o post!

    Pode indicar algum post para session de usuário após o login. Já li alguns posts, mas não estão simples para iniciantes (HttpServlets).

    Obrigado!

  40. Izabela 23/08/2011 at 11:14 #

    Uma questão levantada pelo Jeferson ainda me causa muitas dúvidas. Como não usar tantos IFs em um caso de um List repletos de objetos diferentes (porém todos classes filhas de uma classe abstrata)?
    Por exemplo, tenho uma classe abstrata Instrumento, com inúmeras classes filhas (contendo instrumentos específicos). Em um certo ponto de decisão, tenho consultas a DAOs diferentes dependendor de cada classe filha. -> InstanceOf?

  41. Paulo Silveira 23/08/2011 at 14:23 #

    Ola Izabela

    Realmente, algumas raras vezes precisamos fazer ifs com instanceofs. Devemos fazer com cuidado. Provavelmente quando chegamos mais proximos da infraestrutura, tal é o caso do DAO que voce citou.

    No caso de regra de negocio, ha sempre como evitar colocando um metodo na classe mae/superinterface. La teriamos, por exemplo, um getValorDoFrete, que calcula o frete dependendo do Instrumento musical…. assim nao precisamos colocar essa logica de negocio dentro de um monte de if encadeado.

  42. J.C Alves Netto 15/12/2011 at 11:14 #

    Muito bom, conseguiu através de simples exemplos explicitar a importância da orientação a objetos.

  43. Rogerius 21/03/2012 at 11:33 #

    Muito bom o artigo…

  44. Daniel Wildt 26/03/2012 at 17:11 #

    Realmente o Anti If Campaign é algo muito interessante. http://www.antiifcampaign.com/
    Quando conheci o movimento, começou a ficar mais fácil mostrar para pessoas o impacto que a falta de OO causa em um código fonte.

    Locais onde podemos praticar este tipo de situação são os famosos Coding Dojos. Ter exemplos onde podemos conseguir criar código e depois reduzir através de aplicação de patterns e foco em código limpo e mais fluente.

    Excelente post!

  45. Hugo Sousa 23/06/2016 at 16:06 #

    É delicado esse assunto. Já escutei um programador falar que se visse um if em um código já era indício de mal modelagem e o desenvolvedor não estaria usando OO. Em OO se o projeto for bem modelado não é necessário usar if/else.

    Eu não tenho experiência para falar com precisão sobre isso. Portanto, eu gostaria de saber a opinião de vocês.

Deixe uma resposta