Esse artigo faz parte da revista Java Magazine edição 46. Clique aqui para ler todos os artigos desta edição

Atenção: por essa edição ser muito antiga não há arquivo PDF para download.Os artigos dessa edição estão disponíveis somente através do formato HTML. 

Mão na Massa

Refactoring: da Teoria à Prática

Melhorando a estrutura do seu código-fonte

 

Utilizando técnicas de refactoring para simplificar a manutenção e melhorar a qualidade e a evolução do seu código

 

O refactoring (ou refatoração) é uma atividade comum em processos ágeis como o Extreme Programing, nos quais as mudanças no código são constantes. Essa técnica, no entanto, não é utilizada apenas em softwares desenvolvidos sob esses processos. Seu uso é muito mais abrangente e traz como conseqüência direta um código mais limpo, de manutenção mais fácil e com maior qualidade.

Neste artigo veremos os conceitos de refactoring – quando refatorar uma aplicação e como fazê-lo – e nos apoiaremos nos comandos de refatoração do IDE Eclipse para demonstrar tais tarefas. Embora seja usado um IDE para automatizar alguns passos, o enfoque deste artigo será nas atividades de refatoração em si, de forma independente de ferramentas, bem como nos conceitos fundamentais envolvidos.

 

O Contexto

Quem nunca se deparou com um código no qual a alteração de um simples método levou à modificação de dezenas de linhas de código? E quando foi necessário manter um método "faz tudo" com 500 linhas? Isso sem falar daqueles métodos com dezenas de parâmetros, códigos duplicados e algoritmos que ninguém consegue entender.

Esses sintomas são denominados "badsmells" (maus cheiros) pelos especialistas em refactoring, e resultam principalmente de um design ruim, ou mesmo da falta de design. Os problemas ficam evidentes quando se deseja incluir uma nova funcionalidade ou alterar o código existente: normalmente é difícil (ou até impossível) mudar o código sem quebrar o funcionamento do programa.

 

Mas por que alterar?

Suponha que você vem utilizando um componente qualquer, que tem funcionado adequadamente ao longo de vários anos. Durante esse período, o componente foi usado em diversos sistemas e demonstrou funcionamento e performance adequados, o que o tornou bastante confiável.

Agora seu componente será utilizado em um novo sistemas, e para isso algumas alterações deverão ser efetuadas por questões de compatibilidade. O autor do componente não está mais na empresa, e coube a você assumir a nova tarefa.

Só que, se o componente não tiver sido bem projetado, o que parecia uma alteração simples pode se tornar um pesadelo: sistemas com design ruim são difíceis de alterar. Nestes casos, entender o que deve ser mudado não é uma tarefa simples, e são grandes as chances de se introduzir bugs ao longo do processo.

Cria o código é apenas o início. Todos nós sabemos que os sistemas evoluem, e que o que foi acordado com o cliente hoje nem sempre valerá amanhã. Por isso, código bom não é código que "apenas funciona": é código que também é simples de entender, alterar e estender.

As técnicas de refactoring melhoram o design do software e facilitam seu entendimento. Como conseqüência, o tornam mais simples e facilitam a descoberta de bugs.

 

Entendendo a técnica

Refactoring é uma técnica poderosa, e que precisa ser aplicada com cuidado. O princípio é simples: pequenas alterações são efetuadas de forma incremental no código, enquanto a funcionalidade do sistema permanece inalterada.

A capacidade de refactoring está presente em todos os IDEs modernos, com diferentes graus de sofisticação. Em geral as operações de refactoring automatizadas pelas IDEs são bastante seguras e podem ser efetuadas sem receio.

Dentre os IDEs livres, o Eclipse e o NetBeans destacam-se pela diversidade de comandos, possuindo funcionalidades equivalentes (o plug-in Jackpot aumenta bastante o poder de refactoring do NetBeans, mas deve ser instalado à parte1). Já o IntelliJ IDEA (um produto proprietário) é o IDE com maior capacidade de refactoring existente.

Todas as operações de refactoring disponíveis nas ferramentas de desenvolvimento seguem o catálogo proposto por Martin Fowler (veja as referências ao final do artigo). Porém, como veremos ao longo do texto, nem todas possuem suporte automatizado.

 

Refatorando

A melhor maneira de fixar os conceitos de refactoring é praticando. A Listagem 1 apresenta a classe CartaoUtil. Nela, o método validar() verifica números de cartões de crédito e imprime uma mensagem indicando se o número do cartão é válido ou não. Os parâmetros para validação são a bandeira do cartão (Visa, MasterCard, American Express ou Diners), além de seu número e data de validade.

 

Listagem 1. Classe cartãoUtil original                                                                                    

 

package br.com.jm.refactoring;

 

import java.text.”;

import java.util.*;

 

public class CartaoUtil {

    public static final int VISA = 1;

    public static final int MASTERCARD = 2;

    public static final int AMEX = 3;

    public static final int DINERS = 4;

    public static final String CARTAO_OK = “Cartão válido”;

    public static final String CARTAO_ERRO = “Cartão inválido”;

 

    public String validar(int bandeira, String numero, String validade) {

 

       boolean validade ok = false;

 

      // ----- VALIDADE -----

      Date dataValidade = null;

      try {

          dataValidade = new SimpledateFormat(“MM/yyyy”).parse(validade);

      }   catch (ParseException e) {

           return CARTAO_ERRO;

      }

      Calendar calValidade = new GregorianCalendar();

      calValidade.setTime(dataValidade);

 

      // apenas mês e ano são utilizados na validação

      Calendar calTemp = new GregorianCalendar();

      Calendar calHoje = (GregorianCalendar) calValidade.clone();

      calHoje.set(Calendar.MONTH, calTemp.get(Calendar.MONTH));

      calHoje.set(Calendar.YEAR, calTemp.get(Calendar.YEAR));

 

      validadeOK = calHoje.before(calValidade);

 

      if (!validadeOK) {

          return CARTAO_ERRO;

     }

     else {

           // ---- PREFIXO E TAMANHO -----

           String formatado = “”;

 

          // remove caracteres não-numéricos

          for (int i = 0; i < numero.length(); i++) {

              char c = numero.chartAt(i);

              if (Character.isDigit(c) {

                 formatado += c;

              }

          }

 

          boolean formatoOK = false;

 

          switch (bandeira) {

          case VISA: // tamanhos 13 ou 16, prefixo 4.

             if   (formatado.startsWith(“4”) &&

                  (formatado.length() == 13 ||

                     formatado.length() == 16 )) {

                 formatoOK = true;

             } else {

                formatoOK = false;

             }

             break;

          case MASTERCARD: // tamanho 16, prefixos 51 a 55

             if  ((formatado.startsWith(“51”) ||

                formatado.startsWith(“52”) ||

                formatado.startsWith(“53”) ||

                formatado.startsWith(“54”) ||

                formatado.startsWith(“55”) &&

                formatado.length() == 16) {

              formatoOK = true;

            } else {

              formatoOK = false;

            }

            break;

          case  AMEX: // tamanho 15, prefixos 34 e 37.

            if ((formatado.startsWith(“34”) ||

               formatado.startsWith(“37”) &&

               formatado.length() == 15 ) {

               formatoOK = true;

            } else {

               formatoOK = false;

            }

            break;

          case  DINERS: // tamanho 14, prefixos 300  305, 36 e38.

            if  ((formatado.startsWith(“300”) ||

                formatado.startsWith(“301”) ||

                formatado.startsWith(“302”) ||

                formatado.startsWith(“303”) ||

                formatado.startsWith(“304”) ||

                formatado.startsWith(“305”) ||

                formatado.startsWith(“36”) ||

                formatado.startsWith(“38”) &&

              formatado.length() == 14) {

              formatoOK = true;

            } else {

              formatoOK = false;

            }

            break;

          default:

              formatoOK = false;

              break;

          }

         

          if (!formatoOK) {

             return CARTAO_ERRO;

          }

          else {

             // ----- NÚMERO -----

             // fórmula de LUHN (http://www.merriampark.com/anatomycc.htm)

             int soma = 0;

             int digito = 0;

             int somafim = 0;

             boolean multiplica = false;

 

             for (int 1 = formatado.length() – 1; i >= 0; i--) {

               digito = Integer.parseInt(formatado.substring(i,i+1));

               if (multiplica) {

                somafim = digito * 2;

                if (somafim > 9) {

                    somafim -= 9;

                }

             } else {

                 somafim = digito;

             }

             soma += somafim;

             multiplica = !multiplica;

         }

 

         int resto = soma % 10;

 

         if (resto == 0) {

            return CARTAO_OK;

         } else {

            return CARTAO_ERRO;

            }
        }
    }
  }
}

 

Analisando o exemplo

Aparentemente o código inicial funciona bem, e valida os quatro tipos de cartão corretamente. Uma análise criteriosa, no entanto, mostra uma série de problemas.

O primeiro ponto que merece destaque é que o código não é verdadeiramente orientado a objetos. Percebe-se que não existe a abstração do principal elemento do negócio: o cartão de crédito. As quatro bandeiras são definidas através de constantes numéricas, o que oferece pouca flexibilidade (se uma nova bandeira tiver que ser adicionada, o código terá que ser alterado).

Além disso, a validade do cartão é definida através de um parâmetro do tipo String, que deve obedecer a uma formatação específica: DD/AAAA. Esse tipo de detalhe torna a aplicação "frágil" e aumenta a possibilidade de bugs.

A instrução switch também contribui para que o código se torne procedural. Se você deseja escrever um código realmente orientado a objetos, de evitar essa construção (como veremos posteriormente, em casos como o mostrado o swicht pode ser facilmente substituído pelo uso de polimorfismo).

Outro problema evidente problema evidente é o tamanho do método. Métodos muito extensos são um indicativo de que fazem mais do que deveriam. A quantidade de condições (ifs e elses) torna complicada sua leitura e aumenta o custo de manutenção. A grande quantidade de returns também dificulta a compreensão do método, pois não é simples entender o seu fluxo.

Por fim, utilizar uma mensagem como retorno do método também não é a melhor opção. Imagine que a mensagem tivesse que ser escrita em HTML, ou então ser internacionalizada para outros idiomas. Neste caso, o código teria que ser modificado, embora sua função principal não tenha relação com a interface com o usuário.

 

Passo 1:"enxugando" as construções

Antes de aplicar os padrões do catálogo de refactoring, vamos simplificar um pouco o código. Uma análise rápida mostra dois pontos do programa que são suscetíveis à simplificação: laços e condicionais. Utilizaremos expressões regulares para tornar esses trechos mais limpos.

O laço for a seguir remove caracteres inválidos no número do cartão, conservando apenas os dígitos. O número 5501-7615-8613-3399, por exemplo, seria formatado para 55017615861333992:

 

String formatado ="";

for (int i=0; i<numero.length();i++{

          char c=numero.charAt(i);

          if(Character.isDigit(c)){

                   formatado +=c;

          }

}

 

O código não é difícil de entender, mas apresenta ao menos dois problemas: a quantidade de linhas necessárias para resolver uma tarefa simples e a concatenação de objetos do tipo String. Em Java, como sabemos, Strings são imutáveis. Isso significa que, a cada concatenação, um novo objeto será criado para armazenar o valor do novo texto. Neste código, por exemplo, a variável formato seria recriada dezesseis vezes. Uma possível solução seria utilizar a classe StringBuffer, mas existe uma forma ainda mais simples de resolver o problema.

Utilizando expressões regulares3, podemos solucionar os dois problemas de uma só vez, obtendo um código simples e de funcionalidade idêntica:

         

String formatado=numero.replaceAll(“\\D”,"");

 

O segundo ponto passível de alteração são as instruções case. Cada uma dessas instruções valida o formato do cartão de acordo com a bandeira. Os cartões MasterCard, por exemplo, devem iniciar com os números entre 51 e 55 e possuir 16 dígitos:

 

if((formatado.starsWith("51")||

          formatado.startsWith("52")||

          formatado.startsWith("53")||

          formatado.startsWith("54")||

          formatado.startsWith("55")&&

          formatado.length()==16){

          valido = true;

}else{

  valido = false;

}

 

Todo esse código pode ser substituído por uma expressão regular simples:

 

valido=formatado.matches("^5[1-5]\\d{14}$");

 

Modificando apenas o laço for e as instruções case, já temos uma diminuição significativa na quantidade de linhas e na complexidade do código – veja a Listagem 2. (Nesta e em listagens posteriores, omitimos a parte que não foi alterada depois do refactoring em questão).

Destacamos que neste ponto os casos de teste devem ser re-executados para certificar que o código não foi "quebrado".

Após essa primeira "limpeza", partiremos para operações de refactoring propriamente ditas.

 

Listagem 2. Classe CartaoUtil após o uso de expressões regulares

 

package br.com.jm.refactoring;

 

import java.text.*;

import java.util.*;

 

public class CartaoUtil {

 

    // constants da classe

 

    public String validar(int bandeira, String numero, String validade) {

      

 

       if (!validadeOK) {

          return CARTAO_ERRO;

       }

       else {

          // ----- PREFIXO E TAMANHO -----

         String formatado = numero.replaceAll(“\\D”, “”);

        

         boolean formatoOK = false;

 

         switch (bandeira) {

 

         case VISA; // tamanhos 13 ou 16, prefixo 4.

           formatoOK = formatado.matches(“^4(\\d{12}|\\d{15})$”);

           break;

 

        case MASTERCARD; // tamanho 16, prefixos 51 A 55

           formatoOK = formatado.matches(“^5[1-5]\\d{14}$”);

           break;

 

        case AMEX; // tamanho 15, prefixos 34 e 37

           formatoOK = formatado.matches(“^3[47]\\d{13}$”);

           break;

 

        case DINERS; // tamanho 14, prefixos 300 a 305, 36 e 38.

           formatoOK = formatado.matches(“^3[68]\\d{12}|0[0-5]\\d{11})$”);

           break;

 

        default;

           formatoOK = false;

           break;

        }

 

        If (!formatoOK) {

           return CARTAO_ERRO;

        }

       else {

          // fórmula de LUHN

       }
     }
   }
}

 

Passo 2: alterando o retorno

Como analisado anteriormente, o uso de uma mensagem textual como retorno do método traz uma série de inconvenientes para a evolução do sistema. Para resolver o problema, faremos duas alterações no código; trocaremos o tipo de retorno para boolean e criaremos exceções para os possíveis erros de validação.

Neste passo mudaremos o retorno do método. As exceções serão tratadas no Passo 4.

O retorno do método pode ser alterado através do refactoring Change Method Signature. Para efetuar esse refactoring no Eclipse, selecione o método validar() e escolha a opção de menu Refactor>Change Method Signature. A tela da Figura 1 será exibida. Altere o tipo de retorno para boolean e escolha OK; na próxima tela serão exibidos alguns erros de conversão. Ignore-os e aceite as alterações.

O código refatorado apresentará erros nos locais em que as constantes CARTAO_OK e CARTAO_ERRO são referenciadas, pois essas constantes são do tipo String. Para que o código continue compilável, as constantes deverão ser alteradas manualmente para o tipo boolean4:

 

public static final boolean CARTAO_OK = true;

public static final booelan CARTAO_ERRO = false;

 

Agora que o método retorna um valor booleano indicando o estado do cartão, as constantes CARTAO_OK e CARTAO_ERRO perderam sua utilidade e podem ser removidas. Para remover as constantes de forma segura; usaremos o refactoring Inline constant, que substitui a referência à constante pelo seu valor.

Para realizar a operação basta marcar a constante e selecionar o menu Refactor>Inline. A Figura 2 será exibida. Aceite as opções default e clique em OK. Repita o comando para a outra constante.

 

 

Passo 2: criando métodos auxiliares

O método validar() efetua três testes distintos para decidir se o cartão é válido ou não. Primeiro a data de validade é analisada; caso a data esteja correta, o formato do número é checado (prefixo e tamanho). Por fim, se os dois primeiro testes tiverem sucesso, o formato do número é conferido através da fórmula de LUHN (também conhecida como "Módulo 10").

Essa implementação torna o método grande e aumenta a complexidade da manutenção. É recomendável que cada um desses testes seja separado em um método distinto, facilitando o entendimento e permitindo que futuras alterações possam ser feitas de forma pontual. A separação dos métodos será feita através do refactoring Extract method.

          Para extrair cada método, basta selecionar o trecho que corresponderá ao método e escolher o comando de menu Refactor>Extract Method. Uma tela semelhante à da Figura 3 será mostrada. Preencha o novo nome do método e aceite as opções default.

Neste exemplo, os três novos métodos foram criados com os nomes isValidadeOK(), isFormatoOK() e isNumeroOK().

A nova classe é exibida na Listagem 3. É importante salientar que nenhum refinamento foi feito ainda sobre os métodos criados. Estes métodos são apenas o resultado do refactotring efetuado pelo Eclipse.

 

Listagem 3. Classe CartaoUtil após a extração dos métodos

 

package br.com.jm.refactoring;

 

import java.text.*;

import java.util.*;

 

public class CartaoUtil {

 

    // constantes da classe

 

   public boolean validar(int bandeira, String numero, String validade) {

    

   boolean validadeOK = false;

 

   validadeOK = isvalidadeOK(validade);

 

   if (!validadeOK(validade);

 

     return false;

 

   }

   else {

       // ----- PREFIXO E TAMANHO -----

       String formatado = numero.replaceAll(\\D, “”);

       boolean formatoOK = false;

       formatoOK = isFormatoOK(bandeira, formatado);

 

       if (!formatoOK) {

          return false;

       }

       else {

           return isNumeroOK(formatado);

       }
   }   
}

 

private boolean isValidadeOK(String validade) {

   boolean validadeOK;

   // ----- VALIDAE ------

   Date dataValidade = null;

   try {

       dataValidade = new SimpleDateFormate(“MM/YYYY”).parse(validade);

   } catch (ParseException e) {

      return false;

   }

   Calendar calValidade = new GregorianCalendar();;

   calValidade.setTime(datValidade);

 

      // apenas mês e ano são utilizados na validação

   Calendar calTemp = new GregorianCalendar();

   Calendar calHoje = (GregorianCalendar) calValidade.clone();

   callHoje.set(Calendar.MONTH, calTemp.get(Calendar, MONTH));

   callHoje.set(Calendar.YEAR, calTemp.get(Calendar, YEAR));

 

   validadeOK = calHoje.before(calValidade);

   return validadeOK

}

private boolean isFormatoOK(int bandeira, String formatado) {

   bolean formatoOK;

   switch (bandeira) {

   case VISA; // tamanhos 13 ou 16, prefixo 4.

       formatoOK = formatado.matches(“^4(\\d{12}|\\d{15})$”);

       break;

   case MASTERCARD; // tamanho 16, prefixos 51 A 55

       formatoOK = formatado.matches(“^5[1-5]\\d{14}$”);

       break;

   case AMEX; // tamanho 15, prefixos 34 e 37

       formatoOK = formatado.matches(“^3[47]\\d{13}$”);

       break;

   case DINERS; // tamanho 14, prefixos 300 a 305, 36 e 38.

       formatoOK = formatado.matches(“^3[68]\\d{12}|0[0-5]\\d{11})$”);

       break;

   default;

       formatoOK = false;

       break;

   }

   return formatoOk;

}

 

private boolean isNumeroOK(String formatado) {

     // -----NÚMERO-----

     // fórmula de LUHN (http://www.merriampark.com/anatomycc.htm)

     int soma = 0;

     int digito = 0;

     int somafim = 0;

     boolean multiplica = false;

 

     for (int 1 = formatado.length() – 1; i >= 0; 1--) {

       digito = integer.parseInt(formatado.substring(i,i+1));

       if (multiplica) {

           somafim = digito * 2;

           if (somafim > 9) {

               somafim -= 9;

           }

       } else {

          somafim = digito;

       }

       soma += somafim;

       multiplica = !multiplica;

    }

    int resto = soma % 10;

 

    if (resto == 0) {

        return true;

    } else {

         return false;

    }

  }

}

 

 

 

 

 

 

 

 

 

 

Figura 1. Alterando o retorno do método

 

 

 

 

 

 

 

Figura 2. Removendo as constantes CARTAO_OK  e CARTAO_ERRO.

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Figura 3. Extraindo os métodos auxiliares

 

Passo 3: removendo os parâmetros

Analisando novamente o método validar(), percebemos que todos os parâmetros são, na verdade, atributos de um cartão de crédito. Porém, até o momento essa abstração não existe, e o cartão é representado através do conjunto de parâmetros do método: bandeira, número e validade. Uma evolução natural do código deve contemplar essa classe.

Antes de criar a classe Cartao, trabalharemos nos parâmetros do método validar(). Os três parâmetros serão convertidos em atributos da classe CartaoUtil, e o atributo validade será desmembrado em dois atributos inteiros: mes e ano5 (esse tratamento minimiza eventuais erros de formatação). É importante notar que todas essas operações serão feitas manualmente, já que o Eclipse não oferece suporta automatizado para essa etapa.

Em seguida, a classe CartaoUtil será renomeada para Cartao (visto que agora reflete essa abstração) e um construtor será adicionado, com os novos atributos. Como o método de validação agora não possui mais parâmetros e seu retorno é booleano, vamos renomeá-lo para isValido().

Para renomear a classe e o método, é recomendável utilizar o comando de refactoring Rename (Refactor>Rename), que já efetua todas as alterações na classe e em suas dependentes.

 

Passo 4: adicionado Exceptions

No Passo 1 alteramos o retorno do método para booleano, o que resolveu o problema das mensagens mas trouxe um inconveniente: não é possível saber a causa do erro de validação. Para resolver esse problema usaremos exceções.

As mudanças deste passo seguem o refactoring Replace Error Code with Exception, que não é suportado de forma automática pelo Eclipse. Assim, as exceções deverão ser codificadas manualmente.

 

Para seguir rigorosamente o refactoring Replace Error Code with Exception como definido no catálogo, ele deveria ser feito no primeiro passo, substituindo as mensagens de erro por exceções. Transferimos o refactoring para esse passo para simplificar o entendimento.

 

Criaremos três exceções, que refletem os erros da nossa regras de validação: ValidadeIncorretaException, NumeroIncorretoException e FormatoIncorretoException (Listagens 4,5 e 6). As exceções serão introduzidas nos métodos correspondentes para indicar cada uma das situações de erro. É importante salientar que todas as exceções são do tipo RuntimeException, o que significa que seu tratamento não é obrigatório no código chamador.

 

A substituição de códigos de erro por RuntimeExceptions deve ser feita de forma cuidadosa, pois é fácil introduzir bugs. Uma estratégia é, num primeiro momento, utilizar exceções checadas e verificar pontos do código onde eventualmente será necessário utilizar blocos finally. Após essa etapa, é seguro transformar as exceções em RuntimeExceptions.

 

As mudanças correspondentes aos Passos 3 e 4 estão refletidas na Listagem 7.

 

Listagem 4. Classe ValidadeIncorretaException

 

package br.com.jm.refactoring.excecao;

 

import br.com.jm.refactoring.Cartao;

 

public class ValidadeIncorretaException extends RuntimeException {

    public ValidadeIncorretaException (Cartao cartao) {

       super(cartao.getMes() + “/” + cartao.getAno());

    }

}

 

Listagem 5. Classe NumeroIncorretoException

 

package br.com.jm.refactoring.excecao;

 

import br.com.jm.refactoring.Cartao;

 

public class NumeroIncorretoException extends RuntimeException {

    public NumeroIncorretoException (Cartao cartao) {

       super(cartao.getNumero());

    }

}

 

Listagem 6. Classe FormatoIncorretoException

 

package br.com.jm.refactoring.excecao;

 

import br.com.jm.refactoring.Cartao;

 

public class FormatoIncorretoException extends RuntimeException {

    public FormatoIncorretoException (Cartao cartao) {

       super(cartao.getNumero());

    }

}

 

Listagem 7. Classe Cartao (antiga CartaoUtil)

 

package br.com.jm.refactoring;

 

import java.text.*;

import java.util.*;

import br.com.jm.refactoring.excecao.*;

 

public class Cartao {

     //... Constantes da classe

 

    private int bandeira;

    private String numero;

    private int mes;

    private int ano;

 

    public Cartao(int bandeira, String numero, int mes, int ano) {

        this.bandeira = bandeira;

        this.numero = numero;

        this.mes = mes;

        this.ano = ano;

    }

 

    //getters omitidos

 

    public boolean isValido() {

       ...

    }

 

    private boolean isValidadeOK(int mes, int ano) {

       ...

    }

    private boolean isFormatoOK(int bandeira, String formatado)  {

       ...

    }

    private boolean isNumeroOK(String formatado) {

       ...

    }

 

}

 

Passo 5: switch e classes do modelo

A última mudança no código trata do switch. Como indicado anteriormente, implementar a lógica de validação das bandeiras através desse condicional não é a melhor estratégia; a cada nova bandeira a classe Cartao deverá ser alterada para adição de um novo case. Além disso, misturar validações distintas em um mesmo método pode prejudicar o entendimento do código.

A solução para esse problema consiste em separar cada validação em uma classe específica (sob uma mesma hierarquia), e através de polimorfismo executar a validação apropriada. Ao todo, serão criadas quatro classes, uma para cada bandeira.

A alteração no código de validação será feita através do refactoring Replace Conditional with Polymorphism. Esse refactoring também não possui suporte automatizado no Eclipse, portanto as mudanças deverão ser feitas manualmente.

A refatoração será efetuada aplicando-se o pattern Template Method. Primeiro será criado o método formato(), responsável por retornar o formato utilizado na validação da bandeira. Esse método será abstrato na classe Cartao (que será transformada em abstrata) e implementado em cada uma das bandeiras.

 

protect abstract String formato();

 

Como o formato será definido através de polimorfismo, o atributo bandeira e as constantes das bandeiras não têm mais valor e serão removidos da classe. Também serão necessárias algumas mudanças no método isFormatoOK() para refletir a nova estratégia:

 

private boolean isFormatoOK(String formatado){

          if(!formatado.matches(formato())){

              throw new FormatoIncorretoException(this);

          }

          else{

            return true;

          }

}

 

Com a nova implementação, o método isFormatoOK() chama o método formato() sempre que precisa verificar o formato de um cartão. Esse método estará implementado em casa uma das bandeiras e será executado conforme a instância criada. Para cartões MasterCard, por exemplo, o método retorna o seguinte formato:

         

protected String formato(){

  return "^5[1-5]\\d{14}$";

}

 

A nova classe Cartao encontra-se na Listagem 8. As classes para cada uma das bandeiras podem ser vistas nas Listagens de 9 a 12.

 

Listagem 8. Classe Cartao após o uso de polimorfismo

 

package br.com.jm.refactoring;

 

import java.text.*;

import java.util.*;

import br.com.jm.refactoring.excecao.*;

 

public abstrat class Cartao {

    private String numero;

    private int mes;

    private int ano;

    public Cartao (String numero, int mes, inte ano) {

       this.numero = numero;

       this.mes = mes;

       this.ano = ano;

    }

 

    //getters omitidos

    public boolean isValido() {

       ...

    }

    private boolean isValidadeOK(int mes, int ano) {

       ...

    }

    protected abstract String formato();

 

    private boolean if FormatoOK(String formatado) {

       if (!formatado.matches(formato())) {

          throw new FormatoIncorretoException(this);

       }

       else {

           return true;

       }

    }

    private boolean isNumeroOK(String formatado) {

       ...

    }

}

 

Listagem 9. Classe para validação de cartões Visa

 

package br.com.jm.refactoring;

 

public class visa extends Cartao {

    public Visa(String numero, int mes, int ano) {

       super(numero, mes, ano);

    }

 

    @Override

    protected String formato() {

       return “^4(\\d{12}|\\d{15})$”;

    }

}

 

Listagem 10. Classe para validação de cartões Mastercard

 

package br.com.jm.refactoring;

 

public class visa extends Cartao {

    public Mastercard(String numero, int mes, int ano) {

       super(numero, mes, ano);

    }

 

    @Override

    protected String formato() {

       return “^5[1-5]\\d{14}$”;

    }

}

 

Listagem 11. Classe para validação de cartões American Express

 

package br.com.jm.refactoring;

 

public class Amex extends Cartao {

    public Amex(String numero, int mes, int ano) {

       super(numero, mes, ano);

    }

 

    @Override

    protected String formato() {

       return “^3[47]\\d{13}$”;

    }

}

 

Listagem 12. Classe para validação de cartões Diners

 

package br.com.jm.refactoring;

 

public class Diners extends Cartao {

    public Diners(String numero, int mes, int ano) {

       super(numero, mes, ano);

    }

 

    @Override

    protected String formato() {

       return “^3[68]\\d{12}|0[0-5]\\d{11})$”;

    }

}

 

Conclusões

Efetuando refactorings progressivos sobre o software, conseguimos torná-lo mais simples e facilitar sua evolução. A criação de bandeiras, por exemplo, tornou-se um processo simples: basta implementar uma nova classe que estenda Cartao e codificar o método formato(). As regras de cada bandeira agora estão descritas em classes específicas, e nenhuma alteração é necessária na classe Cartao.

Obviamente algumas melhorais ainda poderiam ser feitas no código para torná-lo mais robusto como, por exemplo, a alteração do parse no método isValidadeOK(), ou a inclusão de um atributo para representar o titular do cartão. Por questões de espaço, no entanto, não abordaremos essas melhorias aqui.

O refactotring é uma técnica poderosa e de extrema importância. Seu uso ajuda a tornar as aplicações mais simples e extensíveis e até prevenir bugs. A técnica é simples, e possui excelente suporte de IDEs. Seu uso constante, portanto, é altamente recomendável.

 

 

_________________________________________

[1] – O Jackpot está em desenvolvimento e é planejado para incorporação à versão 6.0 do NetBeans.

[2]- Os números de cartões utilizados nesse artigo foram criados de acordo com o algoritmo apropriado (fórmula de LUHN), mas, em princípio, não existem.

[3] – O artigo “Uma Mala Direta com JavaMail” na Edição 39 apresentou o essencial sobre expressões regulares dentro de uma aplicação prática. Você pode ver mais detalhes sobre a sintaxe dessas expressões na JavaDoc da classe java.util.regex.Pattern.

[4] – Uma alternativa é utilizar a opção “quick fix” do Eclipse. Selecione a opção Change type of ‘CARTAO_ERRO’ to ‘boolean’. Ainda assim o valor da constante (false) terá que ser alterado manualmente.

 

Links

refactoring.com/catalog

Catálogo de padrões de refactoring

eclipse.org, netbeans.org, intellij.com

IDEs Eclipse, NetBeans e IntelliJ IDEA

jackpot.netbeans.org

Jackpot

 

Livros

Refactoring: Improving the Design of Existing Code, Martin Fowler e outros (Addison-Wesley) – A “bíblia” de refactoring

 


André Dantas Rocha (andre@codecompay.com.br) é mestre em Engenharia de Software pela USP, arquiteto e sócio da Code Company.