Refatoração de código: melhorando sem quebrar

1. O que é refatoração e por que ela é necessária

Refatoração é o processo de modificar o código-fonte de um sistema sem alterar seu comportamento externo observável. A definição formal, cunhrada por Martin Fowler em seu livro clássico, estabelece que refatorar significa aplicar pequenas transformações que preservam a semântica, melhorando a estrutura interna.

É crucial distinguir refatoração de outras atividades:
- Refatoração: muda a estrutura interna, comportamento externo permanece idêntico
- Reescrita: substitui completamente o sistema por uma nova implementação
- Correção de bugs: altera o comportamento esperado para corrigir falhas

Os benefícios são profundos: código mais legível, menor custo de manutenção, redução da dívida técnica e facilitação para adicionar novas funcionalidades no futuro. Um código bem refatorado comunica intenção, não apenas instruções para o computador.

2. Princípios fundamentais para refatorar sem quebrar o sistema

Antes de tocar em uma linha de código, três princípios devem estar enraizados:

Testes automatizados primeiro: O ciclo red-green-refactor é a base. Escreva um teste que falhe (red), faça-o passar da forma mais simples (green), então refatore (refactor). Sem testes, você está apenas editando código cegamente.

Mudanças incrementais: Trabalhe em "baby steps" — alterações tão pequenas que, se algo quebrar, você sabe exatamente o que causou. Cada commit deve manter o código compilando e os testes passando.

Funcionamento contínuo: O sistema deve permanecer operacional a cada etapa intermediária. Se você precisa de 10 passos para refatorar, cada um desses passos deve produzir um sistema funcional.

3. Estratégias de refatoração passo a passo

Extrair métodos para reduzir complexidade

Considere este código antes da refatoração:

function processarPedido(pedido) {
    let total = 0;
    for (let item of pedido.itens) {
        if (item.quantidade > 0 && item.preco > 0) {
            total += item.quantidade * item.preco;
            if (item.categoria === "ELETRONICO") {
                total += total * 0.15; // imposto eletrônico
            }
        }
    }
    if (total > 1000) {
        total -= total * 0.10; // desconto para compras grandes
    }
    return total;
}

Após extrair responsabilidades:

function calcularTotalItens(itens) {
    return itens
        .filter(item => item.quantidade > 0 && item.preco > 0)
        .reduce((acc, item) => {
            let subtotal = item.quantidade * item.preco;
            subtotal += calcularImposto(item, subtotal);
            return acc + subtotal;
        }, 0);
}

function calcularImposto(item, subtotal) {
    return item.categoria === "ELETRONICO" ? subtotal * 0.15 : 0;
}

function aplicarDesconto(total) {
    return total > 1000 ? total * 0.90 : total;
}

function processarPedido(pedido) {
    const totalBruto = calcularTotalItens(pedido.itens);
    return aplicarDesconto(totalBruto);
}

Substituir condicionais aninhadas por early returns

Antes:

function validarUsuario(usuario) {
    if (usuario) {
        if (usuario.email) {
            if (usuario.email.includes('@')) {
                return true;
            }
        }
    }
    return false;
}

Depois:

function validarUsuario(usuario) {
    if (!usuario) return false;
    if (!usuario.email) return false;
    if (!usuario.email.includes('@')) return false;
    return true;
}

4. Refatoração de estruturas de dados e acoplamento

Substituir arrays genéricos por objetos com significado

Antes:

const cliente = ["João", "joao@email.com", "Rua A, 123", "São Paulo"];
// acesso por índice: cliente[1] é o email

Depois:

class Cliente {
    constructor(nome, email, endereco, cidade) {
        this.nome = nome;
        this.email = email;
        this.endereco = endereco;
        this.cidade = cidade;
    }
}

const cliente = new Cliente("João", "joao@email.com", "Rua A, 123", "São Paulo");
// acesso por propriedade: cliente.email

Aplicar injeção de dependência

Antes — acoplamento rígido:

class RelatorioService {
    constructor() {
        this.db = new Database();
    }

    gerarRelatorio() {
        return this.db.buscarDados();
    }
}

Depois — baixo acoplamento:

class RelatorioService {
    constructor(database) {
        this.db = database; // injeção via construtor
    }

    gerarRelatorio() {
        return this.db.buscarDados();
    }
}

// Uso: new RelatorioService(new Database())
// Teste: new RelatorioService(new MockDatabase())

5. Refatoração de código legado sem testes existentes

Quando o sistema não possui testes, a abordagem muda drasticamente. A técnica de caracterização é essencial: escreva testes que capturem o comportamento atual, mesmo que esse comportamento pareça errado. O objetivo é documentar o que o sistema realmente faz.

Costuras (seams) são pontos onde você pode alterar o comportamento sem modificar o código diretamente. Por exemplo, substituir uma chamada de método por uma versão mockada em testes.

Sprout method: quando você precisa adicionar uma funcionalidade em código legado, crie um novo método separado e chame-o do código existente. Isso isola a nova lógica sem modificar a estrutura original.

// Código legado — não toque ainda
function calcularTotal(pedido) {
    let total = 0;
    // 200 linhas de lógica confusa
    return total;
}

// Sprout method — novo código separado
function validarEstoque(pedido, estoque) {
    return pedido.itens.every(item => estoque[item.id] >= item.quantidade);
}

// Ponto de integração mínimo
function processarPedido(pedido, estoque) {
    if (!validarEstoque(pedido, estoque)) {
        throw new Error("Estoque insuficiente");
    }
    return calcularTotal(pedido);
}

6. Como garantir que nada quebrou

Após cada pequena mudança, execute a suíte completa de testes. Ferramentas como Jest, pytest ou JUnit devem rodar em segundos para feedback rápido.

Golden master testing: capture a saída completa do sistema antes da refatoração (arquivos, logs, respostas de API). Após as mudanças, compare caractere por caractere. Qualquer diferença indica regressão.

Análise estática com ESLint, SonarQube ou PMD detecta problemas estruturais que podem indicar refatorações mal-sucedidas, como aumento repentino de complexidade ciclomática.

7. Armadilhas comuns e como evitá-las

Misturar escopos: nunca refatore e adicione funcionalidades no mesmo commit. São atividades que exigem mentalidades diferentes. Refatorar exige cautela; adicionar funcionalidades exige criatividade. Juntas, criam confusão.

Otimização prematura: durante a refatoração, foque em clareza, não em performance. "Programadores desperdiçam tempo pensando ou se preocupando com a velocidade de partes não críticas de seus programas" (Donald Knuth). Otimize depois, com dados de perfilamento.

Efeitos colaterais em código compartilhado: variáveis globais, singletons e estado mutável são os maiores vilões. Antes de refatorar, identifique e isole esses pontos. Use funções puras sempre que possível.


Refatorar é uma disciplina que exige paciência, testes e visão clara do que se deseja alcançar. Quando feita corretamente, transforma código frágil em um sistema robusto e expressivo, sem nunca quebrar o que já funciona.

Referências