Refatoração de Código

Conceito

Podemos facilmente entender o conceito através de um exemplo factível do que vem a ser refatoração. Quando muitas pessoas aprendem programar a qualidade de código gerada por elas não é a ideal, forçando um aprimoramento do programa gerado, começando pelo código. Então a Refatoração o processo de alteração de um sistema de software de modo que o comportamento observável do código não mude, mas que sua estrutura interna seja melhorada. O aperfeiçoamento do código minimiza chances de falhas e tornar o código mais fácil de entender e modificar.
Qualquer alteração de código que não vise estes objetivos, não pode ser considerada uma refatoração. Ao programar, o desenvolvedor poderá ter um programa que execute normalmente, mas que é difícil de entender por uma deficiência de código qualquer. A refatoração deve necessariamente ajudar a tornar o código mais legível, corrigindo deficiências estruturais.

Princípios

Ao refatorar um código devemos levar em consideração que o desenvolvedor já esteja habituado a programar e que já possua alguma experiência na área de testes. No DADT, devido à experiências anteriores, sem um conjunto organizado e específico de testes não existe sentido refatorar. Qualquer conjunto de código que venha a ter uma parte refatorada DEVE ter um conjunto sólido de testes.

Os Coordenadores do CADT devem gastar o tempo que for necessário para criação de testes, juntamente com um ou mais desenvolvedores diretos do código de trabalho. Sendo que esses testes terão de testar a sim mesmos.

Refatoração e Desempenho

Para que um software refatorado, além obter os benefícios desta técnica, tenha um desempenho favorável, há três abordagens que ajudam o programador nesta tarefa. São elas:
A refatoração terá de ser feita em pequenos passos, pois caso seja cometido um erro será fácil encontrar a falha. O uso de ferramentas para tal atribuição também deve ser estimulada entre os desenvolvedores. O ambiente Eclipse, IDE padrão para o DADT, já contém uma ferramenta específica para Refatoração (vide Figura 1).

Figura 1 – Menus de refatoração do Eclipse

Quando usar a Refatoração

O uso da Refatoração para o DADT deve ser algo constante no trabalho de desenvolvimento. Não poderá ser alocado tempo em Cronogramas para a atividade de Refatoração. Essa função tem de ser habitual ao programador e deverá ser estimulada pelos Coordenadores.

Algumas regras devem ser seguidas:

  • Refatorar quando acrescentar novas funções;
  • Refatorar quando necessitar consertar falhas;
  • Refatorar quando revisar o código.

Entender que programas difíceis de ler são difíceis de modificar. O uso de comentários e a documentação do projeto favorece o entendimento e a modificações futuras, seguindo a refatoração. Também evitar o desenvolvimento de lógica duplicada, por nome ou outros entendimentos e códigos com lógicas condicionais complexas.

Orientações aos Professores Gestores e Orientadores

Não deve em hipótese alguma ser uma pessoa voltada exclusivamente a prazos, quando o assunto é refatoração. A documentação dos projetos deve seguir orientações da Coordenação de Processos.Ficando claro que núcleo sem documentação é núcleo falido. Devem ser alocadas seções técnicas nas reuniões periódicas em que se discuta a refatoração.

Indireção e Refatoração (retirado do artigo de Kent Beck, 2006 – com modificações)

A refatoração tende a dividir métodos e objetos maiores em diversos outros menores, chamando isso de indireção. No entanto isso pode ter seu lado bom ou ruim. O lado ruim é que cada vez que divide algo em várias partes terá conseqüentemente mais coisas para gerenciar. Tornando o programa mais difícil de ler. Por outro lado temos vários benefícios:

*Permite compartilhamento de lógica:* Exemplo, um submétodo invocado em dois lugares diferentes ou um método em uma superclasse compartilhado por todas as subclasses.

*Expor intenção e implementação separadamente:* Escolher o nome de cada classe e de cada método dá a oportunidade de explicar o que pretende. O interior da classe ou método corresponde à realização dessa. Se o interior das classes e métodos também estiver escrito em termos de intenção em pedaços menores, poderá escrever o código que descreve, de forma eficiente, grande parte das informações importantes sobre sua própria estrutura.

*Isolar alterações:* Exemplo, o desenvolvedor usa um objeto em dois lugares diferentes e quer alterar o comportamento em um dos casos. Se alterar o objeto arrisca alterar o comportamento de ambos. Então, primeiro deve criar um subclasse e fazer referencia a essa subclasse no caso, a que está sendo alterada. Agora poderá modificar a subclasse sem arriscar uma alteração inadvertidamente.

Problemas com a Refatoração

Apesar da técnica de refatoração de um código trazer benefícios para este, pode também acarretar em alguns problemas. Problemas de migração de banco de dados, pelo fato da maioria das aplicações serem dependentes dos seus esquemas. E em banco de dados orientado a objetos, como a migração não é automática, o programador tem que ter cuidado quando for alterar as estrutura de dados das classes. Outro problema também é a mudança na interface de um programa, pois ao alterar uma interface publicada, o programador precisa manter a interface antiga e a nova até que seus usuários se adequem à mudança.

Refatoração e Desempenho

Para que um software refatorado, além obter os benefícios desta técnica, tenha um desempenho favorável, há três abordagens que ajudam o programador nesta tarefa. São elas:

  • Orçar o tempo: Mais usado em tempo real. Esta abordagem decompõe o projeto à medida que vai avaliando o tempo e quantidade de memória exigida para que, os recursos utilizado, não ultrapasse o limite permitido. O tempo é enfatizado como medida de desempenho. Tal abordagem é frequentemente usada em sistemas de informação corporativos.
  • Atenção constante: Cada programador faz o possível para manter o desempenho alto. Apesar de ser uma abordagem intuitiva não funciona muito bem, pois alterações que melhoram o desempenho normalmente tornam o programa mais difícil de trabalhar. Isso diminui a velocidade de desenvolvimento e o resultado final não deixa o software mais rápido.

Há uma estatísca que mostra que, em um código fatorado uniformente, 90% das suas otimizações são desperdiçadas, pois pode haver melhoria em trechos de códigos que não são relevantes para o programa.

  • A terceira abordagem é fundamentado nessa estatística: O programa é construído de um modo bem fatorado, sem prestar atenção ao desempenho, usando um processo específico para ajustar o programa. Em seguida o programa deve ser executado sob um medidor que monitora e retorna onde o programa está consumindo mais tempo e espaço. Com isso focaliza-se os pontos necessários de fatoração e usa as mesmas otimizações da abordagem da atenção constante. Após cada passo o programa deve ser compilado, testado e executado o medidor. Tal processo deve continuar até chegar ao desempenho desejado pelos usuários.

A principal diferença entre a segunda e a terceira abordagem é que na terceira é fatorada apenas trechos de códigos que vão interferir diretamente no desempenho do programa, enquanto a segunda fatora todo o programa, desperdiçando o tempo gasto para tornar o programa mais rápido. Um programa bem fatorado ajuda na terceira abordagem de duas maneiras:

  • Lhe dá tempo para gastar com ajustes de desempenho.
  • Você tem um código mais selecionado para suas análises de desempenho.

A refatoração além de ajudar a escrever software rápido, diminui a velocidade do software a curto prazo, torna-o mais fácil de ajustar durante a sua otimização.

Exemplos de Refatoração

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 validadeOK = 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.charAt(i);
if(Character.isDigit(c)){
formatado +=c;
}
}

boolean formatoOK = false;
switch (bandeira) {
case VISA: // tamanhos 13 ou 16, prefixo 4.
if (formatado.startsWith("") && (formatado.length() == 13 ||
formatado.length() == 16 )) {
formatoOK = true;
} else {
formatoOK = false;
}
break;
case MASTERCARD: // tamanho 16, prefixos 55
if ((formatado.startsWith("") ||
formatado.startsWith("") ||
formatado.startsWith("") ||
formatado.startsWith("") ||
formatado.startsWith("") &&
formatado.length() == 16)) {
formatoOK = true;
} else {
formatoOK = false;
}
break;
case AMEX: // tamanho 15, prefixos 34 e 37.
if ((formatado.startsWith("") ||
formatado.startsWith("") &&
formatado.length() == 15 )) {
formatoOK = true;
} else {
formatoOK = false;
}
break;
case DINERS: // tamanho 14, prefixos 300 305, 36 e38.
if ((formatado.startsWith("") ||
formatado.startsWith("") ||
formatado.startsWith("") ||
formatado.startsWith("") ||
formatado.startsWith("") ||
formatado.startsWith("") ||
formatado.startsWith("") ||
formatado.startsWith("") &&
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 i = 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 switch 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 (*if*s e *else*s) 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 55017615861333991:

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 regulares2, 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 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 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 2 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.

Figura 2 – Alterando o retorno do método

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 boolean3:

 public static final boolean CARTAO_OK = true;
 public static final boolean 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 3 será exibida. Aceite as opções default e clique em OK. Repita o comando para a outra constante.

Figura 3 – Removendo as constantes CARTAO_OK e CARTAO_ERRO

Passo 3: 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 4 será mostrada. Preencha o novo nome do método e aceite as opções default.

Figura 4 – Extraindo os métodos auxiliares

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(, "");
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 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 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 &gt;= 0; 1--) {
digito = integer.parseInt(formatado.substring(i,i+1));
if (multiplica) {
somafim = digito * 2;
if (somafim &gt; 9) {
somafim -= 9;
}
} else {
somafim = digito;
}
soma += somafim;
multiplica = !multiplica;
}
int resto = soma % 10;
if (resto == 0) {
return true;
} else {
return false;
}
}
}[java]
<strong>Passo 4: removendo os parâmetros</strong>

Analisando novamente o método <strong>validar()</strong>, 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 <strong>Cartao</strong>, trabalharemos nos parâmetros do método <strong>validar()</strong>. Os três parâmetros serão convertidos em atributos da classe <strong>CartaoUtil</strong>, e o atributo validade será desmembrado em dois atributos inteiros: mes e ano4 (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 <strong>CartaoUtil</strong> será renomeada para <strong>Cartao</strong> (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 <strong>isValido()</strong>.

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

<strong>Passo 5: adicionado Exceptions</strong>

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 <em>refactoring</em> <strong>Replace Error Code with Exception</strong>, que não é suportado de forma automática pelo Eclipse. Assim, as exceções deverão ser codificadas manualmente.

Para seguir rigorosamente o <em>refactoring</em> <strong>Replace Error Code with Exception</strong> 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: <strong>ValidadeIncorretaException</strong>, <strong>NumeroIncorretoException</strong> e <strong>FormatoIncorretoException</strong> (<strong>Listagens 4,5 e 6</strong>). 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 <strong>RuntimeException</strong>, o que significa que seu tratamento não é obrigatório no código chamador. A substituição de códigos de erro por <strong>RuntimeExceptions</strong> 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.

<strong>Listagem 4.</strong> Classe ValidadeIncorretaException
[java] 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());
}
}[java]
<strong>Listagem 5</strong>. Classe NumeroIncorretoException
[java] 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 6: 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 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})$";
}}[java]
<strong>Listagem 10</strong>. Classe para validação de cartões Mastercard
[java] 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 possuiexcelente suporte de IDEs. Seu uso constante, portanto, é altamente recomendável.

Observação – Os números de cartões utilizados nesse documento foram criados de acordo com o algoritmo apropriado (fórmula de LUHN), mas, em princípio, não existem. Uso de expressões regulares dentro de uma aplicação pode ser obtida na JavaDoc da classe java.util.regex.Pattern. Uma alternativa é de Refactoring é 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.

Bad Smells

Tendo em mente a importância da refatoração e como ela funciona é importante, também, saber quando e onde usá-la. Ao olhar um código e encontrar estruturas de códigos que insinuam a necessidade de refatoração , é preciso tomar algumas providências utilizando alguns critérios, bad smells, em que a refatoração já deveria ter sido feita.

Código Duplicado

Quando houver o mesmo código em mais de um lugar no programa o melhor a fazer é unificá-los.

Um dos problemas ao tentar fazer a unificação é encontrar a mesma expressão em dois métodos da mesma classe. A solução é utilizar o Extrair Método – técnica que transforma um trecho de código, agrupado lógicamente, em um método que será denominado de acordo com a sua finalidade – chamando o código nos métodos que os utilizam.

outro caso parecido com o anterior é quando tem a mesma expressão em duas subclasses irmãs. O que pode-se fazer é utilizar o Extrair Método para elimiar a duplicação e em seguida Subir Método na Hierarquia, que consistem em mover para a superclasse os métodos que geram os mesmos resultados. No entanto se o código for apenas parecido, utiliza-se o Extrair Método separando as partes que são semelhantes, podendo usar Criar Método Padrão – Colocar os passos das subclasses difeferentes que tem as mesmas características, mas de formas diferentes, em métodos com a mesma assinatura, de modo que os métodos originais se tornem o mesmo. Podendo, então, substituí-los na hierarquia. No entando se os métodos usam algoritmos diferentes e produzem o mesmo resultado, pode-se utilizar Substituir o Algoritmo, substituindo o corpo do método por um algoritmo novo.

Se um código está repetido em mais de uma classe que não são relacionadas pode-se usar Extrair Classe em uma das classes e aplicá-la nas outras. O importante é o programador decidir que o método esteja em apenas uma classe.

Método Longo

Métodos longos dificultam o entendimento do código. Então toda vez que houver a necessidade de comentar um trecho de código deve-se criar com este um novo método, cuja denominação vai ser de acordo com a finalidade do código.

Geralmente para encurtar um método aplica-se Extrair Método.

Para eliminar as variáveis temporárias de um método, com uma grande quantidade de prâmetros e variáveis temporárias, usa-se Substituir Variável Temporária por Consulta, técnica que ao fazer a substituição das variáveis torna o método acessível em toda a classe, tornando o código mais limpo. Para diminuir a lista de parâmetros utiliza-se Introduzir Objeto Parâmetro e Preservar o Objeto Interio. Na primeira técnica um grupo de parâmetros que tendem a ser passados sempre em conjunto é substituído por um objeto. Na segunda, ao invés de ler diversos valores de um objeto e passá-los como parâmetros na chamada de um método, envia-se ao método o objeto inteiro.

Substituir Método por Objeto Método é uma técnica que transoforma o método, que não pode ser aplicada a técnica Extrair Método, em seu próprio método, usando todas as vairáveis locais como campo desse objeto, podendo assim decompor os método em outros métodos no mesmo objeto. Utiliza-se esta técnica após aplicar as técnicas anteriores e ainda assim obtêm-se uma grande quantidade de variáveis temporárias e parâmetros.

Lembrando que sempre que houver a necessidade de comentar o código deve-se criar um novo método. Condições e laços também podem ser extraídos. Em expressões condicionais pode-se utilizar Decompor Condicional, técnica que em estrutura condicional aninhada (if-then-else) extrai métodos da condição da parte após o then e da parte após o else.

Classes Grandes

Classes com variáveis de instância, código e pacotes de variáveis em execesso torna o código ilegível sendo necessário a eliminação de redundância na classe.

Para classes que tem várias funcionalidades há uma grande possibilidade de haver código duplicado. Nesse caso pode-se utilizar Extrair Classe, técnica para criar uma nova classe movendo os campos e métodos adequados da classe antiga para classe nova. Caso haja a necessidade de criar um novo componete sugere-se que seja utilizada Extrair Subclase, técnica que cria uma subclasse a partir das instâncias utilizadas na classe.

Quando uma classe não usa todas as variáveis de instância o tempo todo talvez pode-se usar Extrair Classe ou Extrair Subclasse.

A técnica Extrair Interface extrai o subconjunto de código para uma interface quando diversos clientes usam o mesmo subconjunto da interface de várias classes que têm suas interfaces em comum. Esta técnica ajuda a determinar como os clientes usam a classe.

Quando trata-se de uma classe de interface gráfica do usuário (GUI – Graphical user Interface) pode-se precisar de manutenção e sincronização de alguns dados duplicados, pois os dados e comportamentos podem ser movidos para um objeto em outro domínio. A técnica usada para esta ação é Duplicar Dados Observados, que consiste em copiar os dados que estão disponíveis apenas em um controle GUI para um objeto do domínio, configurando um Observer para sincronizar ambos os fragmentos de dados.

Lista de Parâmetro Longa

Para sanar este Bad Smell pode-se utilizar as seguintes técnicas:

  • Substituir Parâmetro por Método: Quando os parâmetros estiverem fazendo uma solicitação a um objeto conhecido.
  • Preservar o Objeto Inteiro: Quando pode trocar uma lista de dados colhidas de um objeto pelo próprio objeto.
  • Introduzir Objeto Parâmetro: Quando tem diversos dados sem um objeto lógico.

Quando a lista de parâmentros for longa propositalmente, pode-se abrir uma excessão. Nesses casos, uma possível solução é desempacotar os dados e enviá-los como parâmetros, ficando ciente dos riscos envolvidos.

Alteração Divergente

Este bad smell acontece quando é preciso alterar várias vezes uma classe de diversas maneiras e por motivos diferentes. Para tratar esta deficiência é recomendado identificar possíveis mudanças para um caso isolado e aplicar o Extrair Classe e reunir a alteração em uma nova classe.

Cirurgia com Rifle

Acontece sempre que ao realizar mudanças no código ter que fazer diversos ajustes em classes diferentes. Então para colocar todos ajustes em uma classe só pode-se usar Mover Método e Mover Campo. Sendo a primeira técnica para criar um método similar ao método que está sendo usado por mais recursos de outra classe do que a classe que o hospeda e transformar o método antigo em uma simples delegação ou o remover definitivamente. E a segunda técnica, quando um campo é usado por outras classes, mais do que é usado na classe em que está, cria um novo campo na classe alvo e altera todos os usuários deste campo. Para agrupar uma série de comportamentos pode-se usar também Internalizar Classe, migrando os recursos da classe sem utilidade para outra classe.

A principal diferença entre Cirurgia com Rifle e Alteração Divergente é que, na primeira, a mudança altera muitas as classes, enquanto que na segunda, a classe sofre várias alterações.

Inveja dos Dados

Tendo em mente o objetivo principal de um objeto, empacotar dados e processos são usados nestes dados. A Inveja dos Dados acontece quando um método tem a necessidade de chamar outros métodos em outro objeto para auxiliar na sua finalidade. A solução possível é o Mover Método e Extrair Método.

Quando uma método usa características de diversas classes. O fator determinante para decidir em que classe o método vai permanecer é saber qual delas tem o maior número de dados. Para facilitar essa ação o Extrair Método pode ser usado para dividir o método de acordo com o seu destino.

Grupo de Dados

Ao invés de ter dados agrupados durante todo o código, estes dados deveria ser criados em um objeto próprio. Como primeira medida para procurar tais grupos e transformá-lo em objetos usa-se Extrair Classe. Próximo passo é verificar a necessidade de diminuir as assinaturas dos métodos, caso seja precise usá-se Introduzir Objeto Parâmetro ou Preservar o Objeto Inteiro. Além desses passos pode-se também procurar por Inveja de Dados e aplicar as técnicas indicadas nesse bad smell.

Obsessão Primitiva

Muitos dos tipos primitivos podem ser substituídos por classes, como por exemplo strings para telefones ou códigos postais. O mesmo acontece com atributos e objetos, podendo ser usado Substituir Atributo por Método em atributos individuais. Se o atributo for uma enumeração e se não alterar o comportamento usa-se Substituir Enumeração por Classe, substituindo a enumeração por uma nova classe. Caso tenha condições que dependam da enumeração pode-se usar Substituir Enumeração por Subclasses ou Substituir Enumeração pelo Padrão State/Strategy. Onde a segunda técnica substitui a enumeração por um objeto que represente o estado do objeto original.

Comandos Switch

Um dos problemas do comando switch é que se ele estiver em várias partes do código e quiser alterar alguma condição, ou adicionar, é preciso procurar por todas essas partes e fazer a alteração necessária.

Umas das formas de resolver esse problema é usar polimorfismo, evitando inclusive o uso do switch. Quando o switch for usado com enumeração o polimorfismo ocorre na classe hospedeira da enumeração. Usa-se Extrair Método para extrair o switch e o Mover Método para colocá-lo no lugar necessário. Em seguida decide-se, de acordo com a situação do código, qual das técnicas usar: Substituir Enumeração por Subclasse, Substituir Enumeração Pelo Padrão State/Strategy ou Substituir Comando Condicional por Polimorfismo.

Em casos que o switch afeta apenas um único método não é necessário o polimorfismo. Nesses casos pode-se usar Substituir Parâmetro por Métodos Explícitos, técnica que cria um método separado para cada valor do parâmetro enumerado. Para casos em que as condicionais for um nulo, pode-se usar Introduzir Objeto nulo, onde houver reficicações repetidas de um valor nulo é substituído por um objeto nulo.

Hierarquias Paralelas de Herança

Também é um caso de Cirurgia com Rifle. Este bad smell acontece quando ao criar uma subclasse de determinada classe se faça necessário criar outra subclasse em outra classe diferente. Normalmente os prefixos dos nomes das classes em uma hierarquia são os mesmos dos da hierarquia da outra.

Usar o Mover Método e Mover Campo elimina a duplicação e assegura que as instâncias de hierarquia se refiram a instâncias da outra classe.

Classe Ociosa

A técnica Condensar Hierarquia, que une superclass e classe semelhantes, ajuda a eliminar classes que estão no programa, mas não tem uma utilidade relevante. E quando se trata de componentes sem utilidade usa-se Internalizar Classe.

  • Generalidade Especulativa

São classes que estão no programa com a intenção de que um dia sejam usadas, porém so fazem deixar o programa difícil de entender e de manter. Tais classes só valem a pena deixar no código se realmente estiverem sendo usadas, caso contrário é melhor que sejam removidas. Generalidade Especulativa acontece normalmente quando os únicos usuários de um método/classe forem os casos de teste.

Se uma classe abstrata não estiver sendo útil usa-se Condensar Hierarquia. Caso uma delegação for desnecessária pode-se usar Internalizar Classe. Se um parâmetro não estiver sendo usado pelo corpo do método remova-o usando Remover Parâmetro. Métodos que tenham nome que não condizem com a sua funcionalidade pode-se usar o Renomear Método.

Campo Temporário

Quando existirem variáveis de instâncias que estiverem sendo usadas temporariamente no código ou que recebam valores diferentes para situações diferentes, ou até mesmo que não estejam sendo usadas é recomendado usar Extrair Classe para agrupar tais variáveis um método só. Pode-se também usar Introduzir Objeto Nulo para eliminar código condicional.

Cadeias de Mensagens

A técnica Ocultar Delegação, que cria métodos no servidor para ocultar referênias de classes delegadas de um objeto, é usada quando há uma cadeia de objetos que chamam por outros objetos, que chamam outros objetos e assim sucessivamente. Pode-se aplicar esta técnica em vários pontos da cadeia, porém é melhor verificar a finalidade do objeto usado, ver se consegue usar Extrair Método retirar um pedaço do código que usa este objeto e aplicar Mover Método para levá-lo à cadeia. Caso várias clientes de um objeto da cadeia queiram navegar pelo resto do caminho é necessário criar outra classe.

Intermediário

Quando métade dos métodos delegam para outra classe usa-se Remover Intermediário para que este método intereja diretamentente com o objeto solicitante. Esta técnica faz com que o cliente chame o delegado diretamente quando há classes executando uma quantidade exagerada de delegações simples. Se alguns métodos não estiverem fazendo muita coisa pode-se usar Internalizar Método e inserí-los no objeto requerente. Para transformar o intermediário em uma subclasse do objeto real usa-se Substituir Delegação por Herança fazendo com que possa haver adição de comportamentos sem precisar percorrer toda delegação.

Intimidade Inadequada

Intimidade Inadequada diz respeito ao fato de classes que têm acesso frequente às partes privadas de outras classes. Se diz inadequado pois estas classes gastam tempo desnecessário sondando o que não vai usar.

Tais classes precisam ser separadas e pode-se fazer isso usando Mover Método e Mover Campo. Pode-se tentar também usar Transformar Associação Bidirecional em Unidirecional, eliminando o lado em que a classe não precisa mais os recursos da outra. Para classes que têm interesses em comum usa-se Extrair Classe colocando a parte em comun em um lugar seguro ou pode-se usar Ocultar Delegação deixando com que a classe exista como intermediária.

Heranças podem muitas vezes levar a Intimidade Inadequada. Uma possível solução é apliar Substituir Herança por Delegação.

Classes Alternativas com Interfaces Diferentes

As técnicas que podem ser aplicas neste bad smell são:

  • Renomear Método: Para métodos que tenham as mesmas funcionalidades mas com assinaturas diferentes.
  • Mover Método: Quando Renomear Método não é o bastante, quer dizer que as classes ainda não estão fazendo o suficiente. Então é necessário mover o comportamento para classes até que os protocolos sejam os mesmos.
  • Extrair Superclasse: Usa-se esta técnica para compensar a redundância de código ao utilizar Mover Método.

Biblioteca de Classes Incompleta

Para modificar uma biblioteca de classe adicionando alguma classe pode-se usar Introduzir Método Externo, técnica que cria um método na classe cliente uma instância da classe servidora como seu primeiro parâmetro. Se precisar adicionar vários métodos a esta classe pode-se Introduzir Extensão Local criando uma nova classe que contenha tais métodos. Logo essa extensão se tornará em uma subclasse ou um cápsula da classe original.

Classe de Dados

De modo geral tais classes servem apenas para alocar atributos e métodos de acesso para atributos. São manipuladas por outras classes. Essas classes precisam ter uma finalidade mais consistente. Para que isto aconteça é preciso tomar algumas providências, como:

  • Aplicar Encapsular Campo nos atributos públicos. Tal técnica torna privado os campos públicos e fornece-os como método de acesso.
  • Se os atributos do tipo coleção não estiverem encapsulados propriamente deve-se utilizar Encapsular Coleção, técnica que faz método retornar uma visão apenas da leitura e depois fornece métodos de adição/remoção.
  • Para campos que não devem ser alterados aplica-se Remover Método de Gravação. Esta técnica remove quaisquer métodos de gravação de campos que foram criados e nunca deverão ser alterados.
  • Verificar onde tais métodos estão sendo usados por outras classes e aplicar Mover Método para que o comportamentos fiquem na classe de dados.
  • Em métodos que não podem ser movidos completamente pode-se usar Extrair Método para criar um método que possa ser movido.
  • Nos métodos de acesso pode-se aplicar Ocultar Método.

Herança Recusada

Nem sempre quando uma subclasse, que só utiliza alguns métodos herdados da classe pai, é um bad smell. Apesar do problema existir não quer dizer que ele prejudique o código. Por ser algo irrelevante muitas vezes não vale a pena limpá-lo usando técnicas de Descer Método na Hierarquia e Descer campo na Hierarquia para jogar todos os métodos não usados em uma subclasse irmã.

Porém se a subclasse estiver reutilizando comportamentos mas não quiser suportar a interface da superclasse uma solução é esvaziá-la com a técnica Substituir Herança por Delegação.

Comentários

Se um comentário está explicando o que o código faz o ideal é utilizar Extrair Método. Após a extração ainda houver a necessidade de comentar para explicar o que o método está fazendo aplica-se Renomear Método. Se ainda assim precisar de comentário para explicar regras sobre o estado obrigatório do sistema pode-se aplicar Introduzir Asserção, ou seja, se um trecho de código faz alguma suposição sobre o estado do programa torne essa suposição explicita com uma sentença condicional que presumidamente será verdadeira.

Ao refatorar o código muitos dos comentários já não serão mais ultéis. Comentários apenas são ultéis em código quando deseja-se descrever o que está acontecendo naquele trecho. Um comentário muito útil é quando se explica a decisão tomada ao fazer algo, pois são informações que ajudam na manutenção do código futuramente.

  1. sigma Oct 6th, 2011 @ 11:33 | #-49

    Excelente sua abordagem, com um exemplo sendo desenvolvido de forma simples, demonstrando de forma prática os conceitos. Parabéns, Anderson!

Comments are currently closed.
Trackbacks & Pingbacks ( 0 )
  1. No trackbacks yet.