Pular para o conteúdo

Técnicas de Refactoring

Cada refactoring de Fowler é descrito por: Nome (vocabulário compartilhado), Motivação (quando aplicar), Mecânica (passos ordenados) e Exemplo. A regra de ouro: aplique um passo, rode os testes, repita. Se ficar vermelho, reverta apenas o último passo. Abaixo, as técnicas mais frequentes com exemplo Java; para 2-3 delas, a mecânica detalhada.

flowchart TD
    A[Code Smell detectado] --> B{Qual smell?}
    B -->|Long Method| C[Extract Function]
    B -->|Switch repetido| D[Replace Conditional with Polymorphism]
    B -->|Primitive Obsession| E[Replace Primitive with Object]
    B -->|Long Parameter List| F[Introduce Parameter Object]
    B -->|Feature Envy| G[Move Method]
    C & D & E & F & G --> H[Rodar testes -> verde -> commit]

Motivação: Long Method, trecho que pede um comentário, código duplicado. Smell: Long Method, Duplicated Code.

  1. Crie uma função nova, nomeada pela intenção (o quê), não pela implementação (o como).
  2. Copie o trecho extraído para a nova função.
  3. Verifique variáveis locais usadas no trecho: se lidas, viram parâmetros; se atribuídas e usadas depois, viram valor de retorno.
  4. Substitua o trecho original por uma chamada à nova função.
  5. Rode os testes.
public void imprimirExtrato(Conta conta) {
System.out.println("=== Extrato ===");
System.out.println("Titular: " + conta.getTitular());
double saldo = 0;
for (Movimento m : conta.getMovimentos()) {
saldo += m.getValor();
}
System.out.println("Saldo: " + saldo);
}
public void imprimirExtrato(Conta conta) {
imprimirCabecalho(conta);
double saldo = calcularSaldo(conta);
System.out.println("Saldo: " + saldo);
}
private void imprimirCabecalho(Conta conta) {
System.out.println("=== Extrato ===");
System.out.println("Titular: " + conta.getTitular());
}
private double calcularSaldo(Conta conta) {
return conta.getMovimentos().stream()
.mapToDouble(Movimento::getValor)
.sum();
}

Motivação: o inverso de Extract — quando o corpo é tão claro quanto o nome, ou quando você tem indireção desnecessária e quer reorganizar antes de re-extrair. Substitua a chamada pelo corpo e remova a função.

int avaliacao(Motorista m) { return maisDeCincoEntregas(m) ? 2 : 1; }
boolean maisDeCincoEntregas(Motorista m) { return m.getEntregas() > 5; }
int avaliacao(Motorista m) { return m.getEntregas() > 5 ? 2 : 1; }

Motivação: expressão complexa e difícil de ler. Dê nome a subexpressões.

return pedido.getQuantidade() * pedido.getPrecoUnitario()
- Math.max(0, pedido.getQuantidade() - 500) * pedido.getPrecoUnitario() * 0.05
+ Math.min(pedido.getQuantidade() * pedido.getPrecoUnitario() * 0.1, 100);
double precoBase = pedido.getQuantidade() * pedido.getPrecoUnitario();
double desconto = Math.max(0, pedido.getQuantidade() - 500) * pedido.getPrecoUnitario() * 0.05;
double frete = Math.min(precoBase * 0.1, 100);
return precoBase - desconto + frete;

Motivação: o nome não revela a intenção. É o refactoring mais frequente e mais subestimado. Ferramentas de IDE tornam-no seguro em segundos.

public List<Cliente> get(boolean f) { }
public List<Cliente> buscarClientesAtivos(boolean incluirInadimplentes) { }

Motivação: Large Class / Divergent Change — a classe acumula responsabilidades que mudam por razões diferentes. Separe um conjunto coeso de campos e métodos numa nova classe. Cura direta de violação de SRP.

public class Pessoa {
private String nome;
private String telefoneDDD;
private String telefoneNumero;
public String telefoneFormatado() {
return "(" + telefoneDDD + ") " + telefoneNumero;
}
}
public class Pessoa {
private String nome;
private Telefone telefone;
public String telefoneFormatado() {
return telefone.formatado();
}
}
public class Telefone {
private final String ddd;
private final String numero;
public Telefone(String ddd, String numero) {
this.ddd = ddd;
this.numero = numero;
}
public String formatado() {
return "(" + ddd + ") " + numero;
}
}

Motivação: Feature Envy — o método usa mais dados de outra classe que da própria. Mova-o para onde os dados estão.

public class Fatura {
public double percentualDesconto(Cliente cliente) {
return cliente.getPontos() > 1000 ? 0.15 : 0.0;
}
}
public class Cliente {
private int pontos;
public double percentualDesconto() {
return pontos > 1000 ? 0.15 : 0.0;
}
}

Motivação: Switch Statements / condicional por tipo repetida pelo código. Substitua o switch por uma hierarquia onde cada subtipo implementa seu ramo. Habilita OCP: novos casos entram como novas classes, sem editar código existente.

  1. Crie a hierarquia de classes (interface ou classe base + subclasses por caso).
  2. Se ainda não existe, use Replace Constructor with Factory ou um método fábrica para instanciar o subtipo certo.
  3. Mova o corpo de cada ramo do switch para o método sobrescrito da subclasse correspondente.
  4. Substitua o switch pela chamada polimórfica.
  5. Rode os testes após mover cada ramo.
public class CalculadoraArea {
public double area(Forma f) {
switch (f.getTipo()) {
case CIRCULO: return Math.PI * f.getRaio() * f.getRaio();
case QUADRADO: return f.getLado() * f.getLado();
case RETANGULO: return f.getBase() * f.getAltura();
default: throw new IllegalArgumentException("forma desconhecida");
}
}
}
public interface Forma {
double area();
}
public class Circulo implements Forma {
private final double raio;
public Circulo(double raio) { this.raio = raio; }
public double area() { return Math.PI * raio * raio; }
}
public class Quadrado implements Forma {
private final double lado;
public Quadrado(double lado) { this.lado = lado; }
public double area() { return lado * lado; }
}
public class Retangulo implements Forma {
private final double base;
private final double altura;
public Retangulo(double base, double altura) {
this.base = base;
this.altura = altura;
}
public double area() { return base * altura; }
}

Adicionar Triangulo agora é criar uma classe nova — nenhuma linha existente muda.

Motivação: Primitive Obsession — um primitivo carrega regras de domínio espalhadas. Encapsule-o num tipo próprio. Leva a Value Objects.

public class Cliente {
private String email;
public void setEmail(String email) {
if (!email.contains("@")) throw new IllegalArgumentException();
this.email = email;
}
}
public class Email {
private final String valor;
public Email(String valor) {
if (valor == null || !valor.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) {
throw new IllegalArgumentException("e-mail inválido: " + valor);
}
this.valor = valor;
}
public String valor() { return valor; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Email)) return false;
return valor.equals(((Email) o).valor);
}
@Override
public int hashCode() { return valor.hashCode(); }
}
public class Cliente {
private Email email;
public void setEmail(Email email) { this.email = email; }
}

Motivação: Long Parameter List — parâmetros que sempre viajam juntos. Agrupe-os num objeto.

public List<Lancamento> buscar(LocalDate inicio, LocalDate fim, String moeda) { }
public class Periodo {
private final LocalDate inicio;
private final LocalDate fim;
public Periodo(LocalDate inicio, LocalDate fim) {
this.inicio = inicio;
this.fim = fim;
}
public boolean contem(LocalDate data) {
return !data.isBefore(inicio) && !data.isAfter(fim);
}
}
public List<Lancamento> buscar(Periodo periodo, String moeda) { }

O objeto extraído tende a atrair comportamento (contem), reduzindo Feature Envy ao redor dos campos.

Motivação: variáveis temporárias que guardam o resultado de uma expressão dificultam extrações posteriores. Transforme a temp num método de consulta.

public double preco() {
double precoBase = quantidade * precoUnitario;
if (precoBase > 1000) return precoBase * 0.95;
return precoBase * 0.98;
}
public double preco() {
if (precoBase() > 1000) return precoBase() * 0.95;
return precoBase() * 0.98;
}
private double precoBase() {
return quantidade * precoUnitario;
}

Com precoBase() como consulta, extrair preco() para outra classe deixa de arrastar estado local.

  • Passos grandes — mover cinco métodos de uma vez sem rodar testes entre eles; a mecânica de Fowler é deliberadamente granular.
  • Nomear pela implementaçãoExtract Function deve nomear a intenção (calcularSaldo), não o mecanismo (loopSomando).
  • Polimorfismo prematuro — aplicar Replace Conditional with Polymorphism num switch único e estável adiciona indireção sem retorno.
  • Value Object anêmico — criar Email só para embrulhar a String, sem mover validação nem equals/hashCode, não cura Primitive Obsession.
  • Refatorar sem testes — qualquer técnica aqui pressupõe a rede de Fundamentos de Refactoring.

1. Descreva a mecânica de Extract Function e como decidir parâmetros e retorno.

Resposta

Crie uma função nomeada pela intenção, copie o trecho para ela, trate as variáveis locais (as lidas viram parâmetros; a atribuída e usada depois vira valor de retorno), substitua o trecho por uma chamada e rode os testes. Passos pequenos, testes a cada passo.

2. Por que Replace Conditional with Polymorphism habilita o OCP?

Resposta

Porque cada ramo do condicional vira o método sobrescrito de uma subclasse. Adicionar um novo caso passa a ser criar uma classe nova, sem editar o código existente — exatamente o que OCP pede: aberto para extensão, fechado para modificação. Elimina também os switches duplicados que causariam Shotgun Surgery.

3. O que caracteriza um Value Object bem-feito ao aplicar Replace Primitive with Object?

Resposta

Ele é imutável, valida na construção (falha cedo para estados inválidos), concentra o comportamento antes espalhado e implementa igualdade por valor (equals/hashCode). Só embrulhar o primitivo sem mover validação nem igualdade produz um wrapper anêmico que não cura Primitive Obsession. Ver Value Objects.

4. Quando aplicar Inline Function em vez de Extract Function?

Resposta

Quando a indireção não agrega — o corpo é tão claro quanto o nome — ou quando você quer reunir código espalhado numa única função para reorganizá-lo antes de re-extrair de forma melhor. Inline é o inverso de Extract e ambos coexistem no dia a dia.