Code smells e refactoring

Diagnosticar dificuldades de mudança e transformar o código preservando os seus contratos.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

A loja do jogo calcula o mesmo preço em três classes. Alterar o desconto exige editar as três, com risco de ficarem diferentes. Extrair uma regra comum resolve essa dispersão. Durante a refatoração, o preço cobrado tem de continuar igual.

O que é refactoring

Refactoring, ou refatoração, é uma transformação da estrutura interna que preserva o comportamento observável. Extrair um método ou mover uma regra pode mudar o local do código sem mudar valores, efeitos, erros ou ordem relevante.

Os testes ajudam a detetar alterações, mas não são uma prova completa de equivalência. Antes de transformar, identifica o contrato e os casos que poderiam mudar. Corrigir um erro e refatorar são passos diferentes: a correção muda um comportamento incorreto; a refatoração preserva o comportamento decidido.

Os cheiros mais comuns

Um code smell é um sinal para investigar, não uma demonstração automática de mau código. Não existe um número de linhas que decida sozinho se um método está errado. As apresentações agrupam estes sinais em cinco famílias: código inchado, abusos de orientação a objetos, entraves à mudança, código dispensável e acoplamento indevido. A tabela seguinte cruza exemplos destas famílias com a dificuldade que cada um anuncia.

SinalProblema a investigarTransformação possível
Long MethodVárias intenções e níveis de detalhe no mesmo método.Extract Method, com nomes que expliquem as intenções.
Large ClassRazões independentes para alterar a mesma classe.Extract Class.
Duplicate CodeA mesma regra exige mudanças em vários sítios.Extrair a regra para um responsável comum.
Feature EnvyUma operação depende sobretudo dos dados de outro objeto.Move Method, se esse objeto deve ser responsável pela regra.
Data ClumpsUm grupo de valores viaja sempre junto.Introduce Parameter Object ou um tipo do domínio.
Primitive ObsessionPrimitivos representam conceitos com regras próprias.Criar um tipo que valide e ofereça operações adequadas.
Long Parameter ListMuitas decisões independentes numa chamada.Separar operações ou agrupar dados que pertencem juntos.
Refused BequestUma subclasse recusa operações prometidas pela base.Rever a hierarquia e usar composição.

Feature Envy não se decide contando chamadas. Um serializador consulta muitos dados por função; isso não significa que a serialização deva entrar no modelo. Pergunta que módulo deve conhecer e proteger a regra.

Uma classe de dados pode ser adequada para transportar uma resposta ou representar um valor. Torna-se um problema quando regras do mesmo conceito ficam espalhadas pelos clientes. Uma operação transferir pode pertencer a uma conta; uma vista que consulta o saldo continua a ter uma responsabilidade distinta.

Esta palestra usa exemplos em Ruby para treinar o olhar para smells:

Abrir no YouTube

Aplica a pergunta ao Java: qual a alteração que seria difícil no desenho atual e que pequeno passo a tornaria mais simples?

Mudanças dispersas e dependências

Divergent Change ocorre quando mudanças de naturezas diferentes exigem editar a mesma classe. Shotgun Surgery ocorre quando uma mudança exige pequenas edições em muitas classes. O primeiro sugere separar responsabilidades; o segundo pode sugerir juntar uma regra que ficou dispersa.

Outros sinais ajudam a procurar a causa:

  • Temporary Field: um campo só tem significado durante certas fases e exige verificações constantes.
  • Message Chains: um cliente conhece um percurso como arena.getHeroi().getPosicao().getX(), ficando ligado à estrutura interna.
  • Middle Man: uma classe só delega sem esconder uma decisão útil. Confirma antes se representa uma fronteira ou contrato necessário.
  • Speculative Generality: abstrações e parâmetros para alternativas que não existem e aumentam o esforço de leitura.
  • Dead Code: código sem uso que já não faz parte do contrato.

Um switch não é sempre um smell. Investiga quando a mesma decisão sobre tipo se repete em muitos módulos ou cresce sempre que acrescentas uma alternativa. Nesse caso, polimorfismo pode localizar cada comportamento.

Como refatorar em segurança

  1. Descreve o comportamento a preservar e corre os testes existentes.
  2. Se faltam casos relevantes, acrescenta-os antes da transformação.
  3. Faz uma transformação pequena, como extrair um método.
  4. Compila e corre os testes. Inspeciona efeitos e erros que os testes não cobrem.
  5. Repete até a responsabilidade ficar no local pretendido.

Ferramentas do IDE ajudam a atualizar referências, mas não conhecem todas as regras do domínio. Ao mover um método, confirma a ordem de chamadas, as exceções, o estado partilhado e a visibilidade.

Exemplo: extrair a regra do preço

Neste exemplo próprio, cada unidade custa 200 cêntimos até cinco unidades; as seguintes custam 150. Aceitamos de zero a mil unidades. O preço é um inteiro em cêntimos, por isso não introduzimos arredondamentos de vírgula flutuante.

A primeira versão completa mistura a regra de preço com a impressão. Guarda-a como Main.java se quiseres compará-la localmente:

public class Main {
    static void imprimir(int unidades) {
        if (unidades < 0 || unidades > 1000) {
            throw new IllegalArgumentException("quantidade invalida");
        }
        int preco;
        if (unidades <= 5) {
            preco = unidades * 200;
        } else {
            preco = 5 * 200 + (unidades - 5) * 150;
        }
        System.out.println(preco);
    }

    public static void main(String[] args) {
        imprimir(0);
        imprimir(3);
        imprimir(6);
    }
}

As saídas esperadas vêm da regra: zero custa 0, três custam 3×200=6003 \times 200 = 600 e seis custam 5×200+150=11505 \times 200 + 150 = 1150.

A transformação separa extração, mudança de responsável e simplificação; além dos três casos acima, conserva as fronteiras: 5 unidades custam 1000, 1000 custam 150250, e -1 e 1001 lançam IllegalArgumentException. A mensagem também se mantém. Os casos vêm da tarifa e do intervalo permitido, não da fórmula nova.

Passo 1, extrair sem mudar a fórmula. Move o cálculo tal como está para calcular, com a mesma ramificação:

static int calcular(int unidades) {
    if (unidades < 0 || unidades > 1000) {
        throw new IllegalArgumentException("quantidade invalida");
    }
    int preco;
    if (unidades <= 5) {
        preco = unidades * 200;
    } else {
        preco = 5 * 200 + (unidades - 5) * 150;
    }
    return preco;
}

imprimir passa a System.out.println(calcular(unidades));. Compara os casos válidos e inválidos nas duas versões: os valores, o tipo de exceção e a mensagem têm de coincidir. Os três casos do main não exercitam as exceções.

Passo 2, mudar o responsável sem mudar valores. Cria Tarifa.calcular com esse corpo e delega. Os clientes passam a partilhar uma regra única; o comportamento continua igual.

Passo 3, simplificar a fórmula preservando resultados. Só agora introduz constantes e a forma sem ramos:

A regra separada da apresentação
final class Tarifa {
    private static final int LIMITE = 5;
    private static final int PRECO_INICIAL = 200;
    private static final int PRECO_ADICIONAL = 150;

    static int calcular(int unidades) {
        if (unidades < 0 || unidades > 1000) {
            throw new IllegalArgumentException("quantidade invalida");
        }
        int iniciais = Math.min(unidades, LIMITE);
        int adicionais = Math.max(0, unidades - LIMITE);
        return iniciais * PRECO_INICIAL + adicionais * PRECO_ADICIONAL;
    }

}

public class Main {
    static void imprimir(int unidades) {
        System.out.println(Tarifa.calcular(unidades));
    }

    public static void main(String[] args) {
        imprimir(0);
        imprimir(3);
        imprimir(6);
    }

}
Resultado

Executa o código para ver a saída e os gráficos.

Voltamos a obter 0, 600 e 1150. Para zero a cinco unidades, adicionais é zero e iniciais é a quantidade. Acima de cinco, iniciais fica em cinco e adicionais é o excesso. Mantivemos a rejeição fora do intervalo e a mesma mensagem de erro.

Não alterámos a unidade monetária nem o desconto. Trocar a fórmula por uma que aplica 150 a todas as unidades acima de cinco produziria 900 para seis e seria uma mudança de regra, não uma refatoração.

Outras transformações

Extract Variable dá um nome a uma expressão difícil. Preserva quantas vezes ela é calculada quando há efeitos ou estado que muda. Extrair uma chamada do interior de um ciclo para fora pode mudar o resultado.

Guard Clauses tratam casos que terminam cedo e deixam o percurso normal visível. Mantém a ordem de verificações se ela decide qual erro acontece primeiro.

Move Method aproxima uma regra dos dados que ela precisa. Começa por criar o método no destino e delegar do local antigo; depois atualiza clientes se isso fizer parte da mudança.

Encapsulate Collection impede clientes de contornar as operações do dono. Devolver uma cópia ou uma vista não modificável pode resolver problemas diferentes: uma vista não modificável ainda pode refletir mudanças na coleção original.

Replace Conditional with Polymorphism separa alternativas que variam. Só o faz quando as alternativas têm um contrato comum útil. O objetivo é reduzir a propagação de mudanças, não eliminar toda a condição do programa.

Exercícios

Distinguir simplificação de mudança de tarifa

A tarifa aceita de 0 a 1000 unidades. As primeiras cinco custam 200 cêntimos cada; as adicionais custam 150. Duas propostas pretendem simplificar só o cálculo:

// A
Math.min(n, 5) * 200 + Math.max(0, n - 5) * 150
// B
n * (n > 5 ? 150 : 200)

Qual preserva a tarifa para todas as quantidades válidas? Justifica por intervalos e dá um contraexemplo para a outra. O que deve acontecer à validação e à mensagem de erro durante a refatoração?

Pista
Acima de cinco unidades, as primeiras cinco continuam a custar 200 cada.
Solução

A preserva a regra. Para 0 a 5, min(n, 5) dá n e max(0, n - 5) dá zero. Para 6 a 1000, a primeira parcela é 1000 e a segunda cobra só as unidades adicionais. Com n = 6, A dá 1150 e B dá 900: B aplica retroativamente 150 a todas as unidades. Mantemos a rejeição de -1 e 1001, o tipo IllegalArgumentException e a mensagem original. Uma fórmula equivalente no domínio válido não autoriza aceitar entradas que antes eram rejeitadas.

Confere a tua resposta:

  • Apresentei uma quantidade acima de cinco para a qual B muda o preço.
  • Justifiquei as parcelas da versão A nos dois intervalos.
  • Conservei a validação de 0 a 1000 e os erros observáveis.
Voltar à explicação
A ordem pode fazer parte do comportamento

Um método verifica primeiro nome vazio e depois idade negativa, lançando erros diferentes. Uma refatoração troca a ordem. Quando a entrada viola ambos, o comportamento está preservado?

A tua resposta

Pista
Compara a exceção observada para a mesma entrada.
Mais uma pista
Na primeira versão o nome vence; na segunda pode vencer a idade.
Solução

Percorre a entrada que viola ambos: antes, o erro é de nome; depois da troca, o erro é de idade. Não necessariamente. A mesma entrada pode receber um erro diferente. Conserva a ordem se o contrato a exige ou trata a mudança de regra separadamente.

  • Sim, porque continua a lançar uma exceção.. O erro observado pode mudar, incluindo tipo e mensagem.
  • Não necessariamente, porque pode mudar o primeiro erro apresentado.. A ordem de validação pode fazer parte do contrato.
  • Sim, porque guard clauses não mudam comportamento.. Uma transformação só é refatoração se preservar o comportamento relevante.

Erros frequentes

Ter testes verdes incompletos não demonstra equivalência.
Voltar à explicação
Contar chamadas chega?

Um serializador consulta dez atributos de uma entidade para construir texto. Um colega propõe movê-lo para a entidade porque tem mais chamadas à entidade do que a si próprio. Avalia a justificação.

Pista
Pergunta quem deve conhecer o formato externo.
Mais uma pista
Ler dados é parte da função de um serializador.
Solução

A contagem é um sinal insuficiente. Se o método só traduz a entidade para um formato externo, o serializador pode ser o responsável certo. Mover o formato para a entidade pode juntar razões de mudança independentes. Se o método calcula uma regra do domínio, investiga essa parte separadamente.

Confere a tua resposta:

  • Não decide pela contagem de chamadas.
  • Distingue regra do domínio de formato de serialização.
  • Explica que mover o formato pode introduzir uma responsabilidade independente na entidade.

Erros frequentes

Feature Envy não obriga a mover todo o código que consulta muitos getters.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.