Refatoração Java - Fonte Alura
Técnicas
Exemplo de refatoração simples, evitando usar SET’S
Refatoração com Extract Method
Refatoração Internalizar Método
Refatoração Internalizar Variável Temporária
Refatoração Substituir Variável Temporária Por Consulta
Dividir Variável Temporária
Remover Atribuições A Paramêtros
Substituir Método Por Objeto Método
Refatoração Substituição Algoritmo
Refatoração Mover Método
Exemplo de refatoração simples, evitando usar SET’S
public class Conta { private double saldo;
public Conta(double saldo){ this.saldo = saldo; } public void setSaldo(double saldo){ this.saldo = saldo; } public double getSaldo(){ return saldo; } } |
public class Banco {
public void depositar(Conta c, double valor) { c.setSaldo(c.getSaldo() + valor); }
a.setSaldo(a.getSaldo() - valor); c.setSaldo(c.getSaldo() + valor); } } |
Repetição de chamadas de setSaldo, no contexto faria sentido de se criar um método sacar na classe Conta.sacar e Conta.depositar .
public void sacar(double valor) { this.valor = this.saldo – valor; }
this.valor = this.saldo + valor; } |
|
Classe Banco refatorado:
public class Banco { public void depositar(Conta c, double valor) { c.depositar(c.getSaldo() + valor); } public void realizarTransferencia(Conta a, double valor, Conta c){ a.sacar(valor); c.depositar(valor); } } |
Fica mais ubíquo entender o que ocorre na classe Banco, mas como saber se depois da refatoração o comportamento do sistema se manteve?, resposta através dos testes unitários.
Em Trilha.java verificamos a ocorrência de duplicidade de código:
palestra.agendarPara(horarioCalculado) atividadesAgendadas.add(palestra) |
Refatoração com Extract Method
Para evitar essa duplicidade dentro de uma classe, podemos recorrer a extração de método: selecionar o alvor > botão direito mouse> refacto > extrac method :
Refatoração com Extract Method
Para evitar essa duplicidade dentro de uma classe, podemos recorrer a extração de método: selecionar o alvor > botão direito mouse> refactoring > extrac method :
Você pode escolher um nome para o método de consumo interno da classe em Method Name, em que será substituído pela assinatura do método pela assinatura do método palestraLocalTime(horarioCalculado, palestra);
Refatoração Internalizar Método
Temos um método privado que chama outro lerPalestra, este não é chamado em mais nenhum ponto da classe, quando isso ocorre podemos pensar em refatorar por internalização lerPalestra(String linha) para dentro de lerPalestra(Scanner scanner), para fazer devemos selecionar o método invocado dentro de lerPalestra(Scanner scanner), botão direito > refactor > in-line method. Temos duas opções:
Em que podemos escolher colocar a implementação do metodo lerPalestra(String linha) em todas as invocações ou somente na seleção, podendo inclusive selecionar deletar a declaração do método.
Atualizando a classe, ela ficará assim:
Lembrando de rodar testes unitários para se certificar que a aplicação não quebrou.
Refatoração Internalizar Variável Temporária
Quando dentro um loop temos uma variavel criada como parâmetro dentro do laço sem muita função fora do loop, podemos eliminar a mesma:
Selecionar tempoDeDuracaoEmMinutos > botao direito > refactor >inline
Resultado:
Refatoração Substituir Variável Temporária Por Consulta
Nesse método temos variáveis nome e tempoString que possuem uma lógica não trivial do que estão fazendo, extraindo métodos e colocando abstrações do retorno teríamos uma ideia melhor do farão na lógica da classe, também colocar o FINAL nestas variáveis dará ideia se dentro da classe temos outras locais que façam alteração do resultado destas variáveis, habilitando usar métodos
Em seguida extraímos os métodos:
Depois disso podemos aplicar a intenalização de variável para cada uma delas:
Refactor > inline para nome e tempoString:
Em que ao invés de usar variáveis colocamos a assinatura dos métodos no lugar.
Dividir Variável Temporária
Dentro do método temos uma declaração de variável que recebe valores diferentes, que é válido em situações que você queira explicitamente essas mudanças como dentro de um loop, neste caso tem 2 responsabilidades de nomear 2 trilhas diferentes.
Podemos alterar o nome da variavel nomeDaTrilha com refactor-rename, ao mudar o nome, depois colocamos final para ver tem locais dentro da classe que altere o valor da variável.
Vemos que ocorre erro de compilação por haver mudança no valor da
variável para corrigir o erro, devemo nomear outra variável com
responsabilidade única.
Remover Atribuições A Paramêtros
Neste método temos o subtotal que dentro dele ocorre alterações nos valores de subtotal, caso este subtotal tivesse outros métodos poderíamos ter problemas
Substituir Método Por Objeto Método
Existem casos em que uma classe possa passar por processos como validação por exemplo, na qual se abstrair em outra classe que pudesse fazer uma validação por exemplo, a classe origem ficaria mais concisa.
package dominio; public class ISBN {
if (!validar(isbn)) throw new IllegalArgumentException(String.format("ISBN inválido: %s", isbn)); this.numero = isbn; }
if (isbn == null) return false;
if (isbnSomenteNumeros.length() != 13) return false;
try { int tot = 0; for (int i = 0; i < 12; i++) { int digito = Integer.parseInt(isbnSomenteNumeros.substring(i, i + 1)); tot += (i % 2 == 0) ? digito * 1 : digito * 3; }
if (checksum == 10) checksum = 0;
return checksum == Integer.parseInt(isbnSomenteNumeros.substring(12)); } catch (NumberFormatException nfe) { return false; } }
public String toString() { return numero; } } |
Podemos passar esse método para outra classe de responsabilidade bem definida
A classe passa a ficar mais enxuta com o método validar encapsulado em outra classe.
package dominio; public class Validar {
public Validar(String valor) { super(); this.valor = valor; } public boolean validar() { if (valor == null) return false;
String isbnSomenteNumeros = valor.replaceAll("-", ""); if (isbnSomenteNumeros.length() != 13) return false;
int tot = 0; for (int i = 0; i < 12; i++) { int digito = Integer.parseInt(isbnSomenteNumeros.substring(i, i + 1)); tot += (i % 2 == 0) ? digito * 1 : digito * 3; }
int checksum = 10 - (tot % 10); if (checksum == 10) checksum = 0;
return checksum == Integer.parseInt(isbnSomenteNumeros.substring(12)); } catch (NumberFormatException nfe) { return false; |
A ideia seria de criar uma nova classe que tenha como variaveis, as variáveis do método validar alvo, criar um construtor com todas elas e retirar caso não sejam necessárias.
Refatoração Substituição Algoritmo
Podemos nos deparar com código legado cujas implementações não contemplem as inovações do java 8 como streams, lambdas, high order functions e etc, nesta refatoração podemos lançar mão delas.
package br.com.caelum.livraria.dominio;
import java.util.Iterator; import java.util.List; import java.util.stream.Collectors;
private final List<Livro> lista; public Livros(Livro ... livros) { this.lista = Arrays.stream(livros) .collect(Collectors.toList()); } public Money getSubtotal() { Money subTotal = Money.of(0, Livraria.reais); for(Livro livro : lista) { Money valorDoLivro = livro.getValor(); subTotal = subTotal.add(valorDoLivro); } return subTotal; } @Override public Iterator<Livro> iterator() { return lista.iterator(); } public void adicionar(Livro livro) { this.lista.add(livro); } } |
Resultado
public Money getSubtotal() { return lista.stream().map(livro::getValor).reduce(Money.of(0, Livraria.reais), Money::add); } |
Utilizamos méthod references dentro da expressão mas poderia usar lambda também.
Refatoração Mover Método
package br.com.caelum.livraria.dominio; import java.math.BigDecimal; import org.javamoney.moneta.Money; public class Desconto { private final Money subtotal; private final TipoDeDesconto tipo; public static final Desconto NENHUM = new Desconto(Money.of(0, Livraria.reais), TipoDeDesconto.NENHUM); public Desconto(Money subtotal, TipoDeDesconto tipo) { this.subtotal = subtotal; this.tipo = tipo; } public Money getValor() { Money valor = Money.of(0, Livraria.reais); if(tipo.equals(TipoDeDesconto.CUPOM_DE_DESCONTO)) { valor = subtotal.subtract(subtotal.with(quantia -> quantia.subtract(Money.of(5, Livraria.reais)))); } else if(tipo.equals(TipoDeDesconto.FIDELIDADE)) { valor = subtotal.subtract(subtotal.with(quantia -> quantia.multiply(BigDecimal.ONE.subtract(porcentagem(new BigDecimal(10)))))); } return valor; }
private BigDecimal porcentagem(BigDecimal fatorDeCalculo) { return fatorDeCalculo.divide(BigDecimal.valueOf(100)); } } |
Na classe Desconto temos um método getValor em que para cada Desconto é gerado um novo eles if, tendo este método a crescer indefinidamente, com responsabilidade crescente de gerenciar os diferentes tipos de desconto.
Neste exemplo a classe Desconto recebia um enum de TipoDesconto e fazia os cálculos, agora a lógica de desconto ficará dentro do enumerado.
A classe Desconto ficará desta forma:
package dominio; import org.javamoney.moneta.Money; public class Desconto { private final Money subtotal; private final TipoDeDesconto tipo; public static final Desconto NENHUM = new Desconto(Money.of(0, Livraria.reais), TipoDeDesconto.NENHUM); public Desconto(Money subtotal, TipoDeDesconto tipo) { this.subtotal = subtotal; this.tipo = tipo; } public Money getValor() { return tipo.getValor(subtotal); } } |
O enumerado TipoDeDesconto ficará desta forma:
package dominio; import java.math.BigDecimal; import org.javamoney.moneta.Money; public enum TipoDeDesconto { CUPOM_DE_DESCONTO { @Override public Money getValor(Money subtotal) { return subtotal.subtract(subtotal.with(quantia->quantia.subtract(Money.of(5,Livraria.reais)))); } }, FIDELIDADE { @Override public Money getValor(Money subtotal) { return subtotal.subtract(subtotal.with(quantia->quantia.multiply(BigDecimal.ONE.subtract(porcentagem(new BigDecimal(10)))))); } private BigDecimal porcentagem(BigDecimal fatorDeCalculo) { return fatorDeCalculo.divide(BigDecimal.valueOf(100)); } }, NENHUM { @Override public Money getValor(Money subtotal) { return Money.of(0, Livraria.reais); } public abstract Money getValor(Money subtotal); } } |
Passamos para o enumerado os métodos getValor e porcentagem, o último ficou dentro da lógica FIDELIDADE por ser usado só lá, transformamos o método getValor em abstrato, obrigando que os tipos enumerados passem a fazer sua lógica de cálculo de preço.
Refatoração Mover Campo
public CarrinhoDeCompras adicionarLivroNoCarrinhoDoCliente(ISBN isbn, Cliente cliente, String cep) { Livro livro = todosLivros.por(isbn); Money valorFrete = calculadoraFrete.baseadoEm(cep);
CarrinhoDeCompras carrinho = new CarrinhoDeCompras(cliente, livro, valorFrete, now()); if(carrinhos.contains(carrinho)) { carrinho = carrinhos.stream() .filter(umCarrinho -> umCarrinho.doCliente(cliente)) .findFirst().orElse(null); if(carrinho != null)carrinho.getLivros().adicionar(livro); } else carrinhos.add(carrinho); return carrinho; } |
Este método tem a responsabilidade de adicionar livro no carrinho de compras com alguns parâmetros de cliente como cep e objeto cliente, poderiamos mover cep para dentro de cliente para instanciar um único objeto minimizando a possibilidade de passar dados null no método de interesse.
Pós refatoração, temos:
Na classe cliente, está ficará:
package dominio;
private final String id; private final Telefone telefone; private String cep;
http://this.id = id; this.telefone = telefone; this.cep = cep; }
public boolean equals(Object obj) { boolean iguais = false; if (obj instanceof Cliente) { Cliente outra = (Cliente) obj; iguais = id.equals(outra.id); } return iguais; }
@Override public int hashCode() { return id.hashCode(); }
return telefone.toString(); } public String getCep() { return cep; } |
Refatoração Extração de Classe
Quando um método possui atribuições em demasia, este pode ser candidato a extração de uma nova classe que separe as responsabilidades do método.
Podemos criar uma nova classe para tal:
Esta nova classe chamada de CarrinhodeCompraFactory teve o método obterCarrinho tendo sido extraído na origem como método privado e colado na nova classe como público.
Na classe origem esta ficou assim:
Instanciamos um objeto do tipo private CarrinhodeCompraFactory carrinhoFactory, colocamos o objeto carrinhoFactory que possui agora o método obterCarrinho lá:
CarrinhoDeCompras carrinho = carrinhoFactory.obterCarrinho(cliente, livro, valorFrete); |
Refatoração Internarlizar Classe
Embora seja quase um mantra dividir código e responsabilidades na arquitetura de software, por vezes alguma classe pode perder sua função inicial pela evolução do sistema, sendo que as vezes faça sentido trazer para dentro de uma classe uma classe com esse perfil:
Classe Telefone alvo, classe Cliente como destino:
package dominio;
private final Telefone telefone; private String cep;
http://this.id = id; this.telefone = telefone; this.cep = cep; }
public boolean equals(Object obj) { boolean iguais = false; if (obj instanceof Cliente) { Cliente outra = (Cliente) obj; iguais = id.equals(outra.id); } return iguais; }
public int hashCode() { return id.hashCode(); }
return telefone.toString(); } public String getCep() { return cep; } } |
Para fazer esta migração, temos que trazer as variaveis ddd e numero, tiramos o objeto Telefone do construtor, como também trazer os métodos públicos de Telefone para dentro de Cliente, a Classe Cliente finalizada ficaria assim:
package dominio;
private final String id; private String cep; private String ddd; private String numero;
http://this.id = id; this.cep = cep; this.ddd = ddd; this.numero = numero; }
public boolean equals(Object obj) { boolean iguais = false; if (obj instanceof Cliente) { Cliente outra = (Cliente) obj; iguais = id.equals(outra.id); } return iguais; }
public int hashCode() { return id.hashCode(); }
return getTelefoneFormatado(); }
return cep; }
return String.format("(%s) %s", ddd, numero); } } |
Refatoração Ocultar Delegação
A motivação para usar a técnica de ocultar delegação seria a de dar maior integridade aos objetos, evitando expor métodos internos das classes.
Na classe CarrinhoDeCompras, tinhamos o método getLivros() que retornava um objeto livros para ser adicionado em outra classe (CarrinhodeCompraFactory)
public Livros getLivros() { return livros; } |
Dentro do if a função do getLivro() é fornecer livro que será utilizado em adicionar(livro), poderíamos criar na classe origem um método adicionar que englobe essas responsabilidades.
Na classe CarrinhoDeCompra teria este novo método:
public void adicionar(Livro livro) { livros.adicionar(livro); } |