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);

}


public void realizarTransferencia(Conta a, double valor, Conta c){

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;

}


public void depositar(double 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 {


private final String numero;


public ISBN(String isbn) {

if (!validar(isbn))

throw new IllegalArgumentException(String.format("ISBN inválido: %s", isbn));

this.numero = isbn;

}


private boolean validar(String isbn) {

if (isbn == null)

return false;


String isbnSomenteNumeros = isbn.replaceAll("-", "");

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;

}


int checksum = 10 - (tot % 10);

if (checksum == 10)

checksum = 0;

 

return checksum == Integer.parseInt(isbnSomenteNumeros.substring(12));

} catch (NumberFormatException nfe) {

return false;

}

}


@Override

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 {


private String valor;

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;


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;

}

 

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.Arrays;

import java.util.Iterator;

import java.util.List;

import java.util.stream.Collectors;


import org.javamoney.moneta.Money;


public class Livros implements Iterable<Livro> {

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;


public class Cliente {

 

private final String id;

private final Telefone telefone;

private String cep;


public Cliente(String id, Telefone telefone, String cep) {

http://this.id = id;

this.telefone = telefone;

this.cep = cep;

}


@Override

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();

}


public String getTelefone() {

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;


public class Cliente {


private final String id;

private final Telefone telefone;

private String cep;


public Cliente(String id, Telefone telefone, String cep) {

http://this.id = id;

this.telefone = telefone;

this.cep = cep;

}


@Override

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();

}


public String getTelefone() {

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;


public class Cliente {

 

private final String id;

private String cep;

private String ddd;

private String numero;


public Cliente(String id, String cep, String ddd, String numero) {

http://this.id = id;

this.cep = cep;

this.ddd = ddd;

this.numero = numero;

}


@Override

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();

}


public String getTelefone() {

return getTelefoneFormatado();

}


public String getCep() {

return cep;

}


public String getTelefoneFormatado() {

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);

}