Passo 1: O que são números mágicos Passo 2: Números mágicos no código Passo 3: Como corrigir? Final: Correção com enums?
#PraCegoVer - Transcrição dos Slides Números mágicos são uma má prática de programação
Em venda.setTipo(2);, o que "2" significa para a venda?
Em venda.setFormaPagamento("CARTAO"); faz diferença se usar letras minúsculas?
Por que você não coloca esses valores em constantes ou enums?
Em venda.setTipo(Tipo.Venda.Brinde); sabemos que a venda é do tipo Brinde.
Em venda.setFormaPagamento(FormaPagamento.CARTAO); eliminamos erros de digitação, percebeu? Brinde e CARTAO são enums

O que são números mágicos?

Magic Numbers é uma má prática de programação caracterizada pela presença de valores numéricos ou textuais constantes no código que não sejam auto descritivos. Apesar do nome dado a essa prática nos remeter a números, ele se refere a qualquer valor, seja numérico ou alfanumérico, cujo significado não esteja claro.

Exemplo

A classe Venda, apresentada abaixo, possui os campos total, tipo e formaPagamento, para os quais atribuímos tipos primitivos:


      01 public class Venda {
      02 
      03     private float total;
      04     private int tipo;
      05     private String formaPagamento;
      06
      07     // getters e setters
      08 }

Sendo o Java uma linguagem fortemente tipada, ao instanciar uma venda devemos atribuir os valores dos tipos correspondentes aos métodos setTipo(), setFormaPagamento() e setTotal(), conforme apresentado abaixo:


      01 Venda venda = new Venda();
      02 venda.setTipo(2);
      03 venda.setFormaPagamento("CARTAO");
      04 venda.setTotal(10F);
      

O código acima instancia uma classe, atribuindo a ela um tipo, forma de pagamento e total. A partir dele podemos notar que uma venda possui um tipo, mas apenas imaginar, sem nenhuma certeza, o que o número dois significa para esse campo da classe. Vemos ainda que uma venda possui uma forma de pagamento, mas o que acontecerá se o programador informar a palavra cartão com acento ou em letras minúsculas? Esses são casos de números mágicos no código, pois tais valores deveriam estar mais bem documentados em constantes ou, como veremos neste microexemplo, em enums.

Problemas decorrentes dos números mágicos

São muitos os problemas que os números mágicos trazem para o projeto. Voltando ao exemplo anterior, o número dois atribuído à venda pode gerar insegurança quanto a usar ou não esse valor na criação de uma venda, visto que há incerteza quanto ao comportamento do código que utilizará esse valor. Além disso, toda vez que for preciso lidar com esse tipo de venda, teremos que digitar esse número, o que tornará a manutenção difícil, exigindo mudar mais de um local. Esse contratempo fica ainda mais evidente se olharmos o método validar, da classe VendaValidar:


      01 if (venda.getFormaPagamento().equals("CARTAO") && venda.getTotal() < 100) {
      02     throw new Exception("Valor inválido para forma de pagamento");
      03 }
      04 
      05 if (venda.getTipo() == 2 && venda.getTotal() > 0) {
      06     throw new Exception("Valor inválido para venda");
      07 }

É possível prever o esforço exigido do programador para localizar referências a esses valores que lhes atribuam significado. Outra vulnerabilidade, apresentada na linha 1, diz respeito à forma como o Java interpreta strings. Uma vez que “cartao” e “CARTAO” são palavras diferentes para o compilador, um erro de digitação invalidará a regra estabelecida na primeira condicional. Ainda na linha 1 temos a propagação do número mágico, que teve de ser digitado novamente. O problema se repete na linha 5. Comparar o total da venda com os valores 100 e 0 também é um caso de números mágicos, pois estes valores poderiam ser nomeados com constantes.

Como corrigir essa má prática

Como primeiro passo para eliminar os números mágicos, podemos alterar os campos da classe Venda, atribuindo a eles um tipo não primitivo.

correção para números mágicos

Estabelecemos assim um local comum, as enums TipoVenda e FormaPagamento, do qual seus valores devem ser obtidos. A listagem a seguir apresenta a nova versão dessa classe:


      01 public class Venda {
      02 
      03     private float total;
      04     private TipoVenda tipo;
      05     private FormaPagamento formaPagamento;
      06 
      07     // getters e setters
      08 }

Agora tipo e formaPagamento esperam receber um dos valores definidos nas seguintes enums:


    01 public enum TipoVenda {
    02 
    03     BRINDE(1, "Brinde"),
    04     PADRAO(2, "Padrão");
    05 
    06     private int codigo;
    07     private String descricao;
    08 
    09     TipoVenda(int codigo, String descricao) {
    10         this.codigo = codigo;
    11         this.descricao = descricao;
    12     }
    13 
    14     // getters
    15 }


    01 public enum FormaPagamento {
    02 
    03     CARTAO(1, "Cartão"),
    04     BOLETO(2, "Boleto");
    05 
    06     private int codigo;
    07     private String descricao;
    08 
    09     FormaPagamento(int codigo, String descricao) {
    10         this.codigo = codigo;
    11         this.descricao = descricao;
    12     }
    13      
    14     // getters
    15 }
    

Dessa forma, instanciar a venda torna-se um processo mais assertivo, como podemos notar na listagem abaixo:


      01 Venda venda = new Venda();
      02 venda.setTipo(TipoVenda.BRINDE);
      03 venda.setFormaPagamento(FormaPagamento.CARTAO);
      04 venda.setTotal(10F);

Não há dúvidas agora do significado por trás dos valores passados como parâmetro para os métodos setTipo() e setFormaPagamento(). Outra melhora, menos perceptível, é que após definir uma enum para os campos tipo e formaPagamento, limitamos os possíveis valores para essa propriedade, eliminando ainda as chances de erros de digitação.

Vejamos a seguir o que muda na classe VendaValidar:


      01 public static final float TOTAL_MINIMO_CARTAO = 100F;
      02 public static final float TOTAL_VENDA_BRINDE = 0F;
      03 
      04 public static void validar(Venda venda) throws Exception {
      05     if (venda.getFormaPagamento() == FormaPagamento.CARTAO 
                 && venda.getTotal() < TOTAL_MINIMO_CARTAO) {
      06         throw new Exception("Valor inválido para forma de pagamento");
      07     }
      08 
      09     if (venda.getTipo() == TipoVenda.BRINDE && venda.getTotal() > 
                 TOTAL_VENDA_BRINDE) {
      10         throw new Exception("Valor inválido para venda");
      11     }
      12 }

Como consequência dessas alterações, compreender as regras de validação da venda também se torna mais fácil. Ao ler o código com atenção percebemos que a primeira regra verifica se a venda atingiu o valor mínimo esperado para o pagamento em cartão. Também na segunda validação está claro que uma venda do tipo brinde deve ter seu total igual a zero.

Números mágicos é um code smell e, portanto independe de linguagem. Nomes conhecidos como Martin Fowler e Robert C. Martin têm apresentado diferentes formas de evitá-lo, sendo estes alguns dos recursos disponíveis na linguagem Java para tal.