MÓDULO 00 · CONCEITO 05 DE 5

Code Smells & Refatoração

Reconhecer cheiros é metade do trabalho. Refatorar com segurança é a outra metade.

Tempo de leitura ~24 min Pré-requisito Testabilidade como design Próximo Módulo 01 — Fundamentos II

Existe uma habilidade que separa engenheiros experientes dos demais, e ela não aparece em currículos: a capacidade de olhar para um trecho de código e sentir que algo está errado antes de saber explicar o quê. Essa sensibilidade tem origem prática — vem de ter visto muitos sistemas envelhecerem mal, e de ter passado por uma quantidade suficiente de bugs cuja causa raiz era um código que "parecia ok mas não estava". Kent Beck, em uma conversa com Martin Fowler nos anos 90, deu nome a essa intuição: ele a chamou de code smell. Cheiros são pistas. Não são doenças, são sintomas. E como em medicina, reconhecê-los antes de virarem doença é a diferença entre prevenção e cirurgia.

Fowler formalizou o catálogo no livro Refactoring: Improving the Design of Existing Code (1999, segunda edição em 2018). Ali, ele cunhou outra ideia complementar e igualmente importante: cada cheiro tem uma série de refactorings nomeados que podem ser aplicados como resposta. Um catálogo de cheiros mais um catálogo de transformações, juntos, formam o que hoje chamamos de "engenharia de software disciplinada". Você não conserta código no escuro; identifica o cheiro, escolhe a transformação adequada, aplica em pequenos passos seguros (com testes verdes a cada passo), e verifica o resultado.

Este conceito fecha o Módulo 00 amarrando tudo o que veio antes. SOLID, coesão e acoplamento, composição, testabilidade — todos esses princípios são preventivos: aplicados desde o começo, evitam que o código adoeça. Cheiros e refatoração são curativos: aplicados ao código existente, restauram saúde estrutural. Você precisa dos dois, porque mesmo o código melhor projetado envelhece — requisitos mudam, contextos viram outros, decisões antes corretas hoje são erradas. Refatorar continuamente é o que mantém um sistema vivo por anos.

O que é um cheiro

Um code smell é uma estrutura no código que sugere — sem provar — a existência de um problema mais profundo. A palavra "sugere" é chave: cheiros não são regras absolutas. Um método de cinquenta linhas pode estar perfeitamente bem em alguns contextos. Uma classe com muitos atributos pode ser legítima quando modela uma entidade rica do domínio. O cheiro é um sinal que merece investigação, não correção automática. Fowler insiste neste ponto: a decisão de refatorar é sempre julgamento, nunca regra; o cheiro é só o gatilho.

A utilidade prática dos cheiros é dupla. Como vocabulário, eles permitem que duas pessoas conversem sobre código de forma precisa: dizer "tem um feature envy aqui" comunica mais em três palavras do que três parágrafos descrevendo o problema. Como heurística, eles treinam a atenção: depois que você aprende a reconhecer um cheiro, passa a vê-lo em todo lugar, e pequenas correções viram automáticas. O olho calibrado é o que dá velocidade ao engenheiro experiente — não digitação rápida, mas diagnóstico rápido.

Os cheiros que mais aparecem na prática

O catálogo de Fowler tem cerca de vinte cheiros, e a segunda edição com Kent Beck adicionou mais alguns. Em vez de listar todos, vamos cobrir os que aparecem com frequência alta o suficiente para valer atenção imediata. Quando você abrir um codebase que não conhece, são esses que vai encontrar.

Long Method (método longo)

O cheiro mais óbvio. Um método que ocupa cinquenta, cem, duzentas linhas faz coisas demais — geralmente várias coisas no nível errado de abstração ao mesmo tempo. O sintoma costuma ser comentários separando "seções" do método: "// validação", "// cálculo", "// envio". Cada um desses comentários é um candidato natural a virar uma sub-função com nome próprio. O refactoring canônico é Extract Method: extrair pedaços coesos em métodos nomeados, deixando o método original como uma sequência de chamadas que se lê como um resumo do fluxo.

Um critério prático que vale mais que limites de linha: se você precisa de comentários explicando o que cada bloco faz, esses blocos provavelmente deveriam ser métodos. O nome do método substitui o comentário, e o compilador (ou linter) garante que ele não diverge da implementação como comentários divergem com o tempo.

Large Class (classe grande)

A versão classe do Long Method. Uma classe com vinte, trinta, cinquenta métodos, ou com excesso de campos, geralmente está fazendo o trabalho de duas ou três classes diferentes. O sintoma é familiar: God Object — uma classe que sabe tudo, faz tudo, depende de tudo. Em codebases legados, é sempre OrderManager, UserHelper, ou SystemController. O nome genérico é confissão: a classe não tem uma responsabilidade clara.

A correção via SRP é o mapa: identifique as razões para mudar, separe em classes menores, cada uma respondendo a um stakeholder ou eixo de mudança. Extract Class é o refactoring nomeado. Em prática, isso geralmente acontece em ondas: você extrai a primeira classe óbvia, e a partir dela ficam visíveis outras separações que não eram antes.

Long Parameter List (lista longa de parâmetros)

Quando uma função recebe seis, sete, oito parâmetros, três coisas costumam estar acontecendo. Primeiro, vários desses parâmetros provavelmente formam uma estrutura conceitual junta — um Whole Object escondido (o refactoring que extrai todos eles num único Address, por exemplo). Segundo, alguns parâmetros podem estar sendo passados só para serem repassados adiante — sintoma de que esse método não deveria existir ali. Terceiro, parâmetros booleanos múltiplos (process(order, true, false, true)) revelam que o método faz coisas demais e o caller está configurando comportamento — caso típico para quebrar em métodos separados ou aplicar Strategy.

A heurística pragmática é: três parâmetros é confortável, quatro começa a chamar atenção, cinco ou mais precisa de pausa. Não é regra; é alarme. Algumas funções (cálculos numéricos, parametrizações genuínas) naturalmente recebem muitos valores e está tudo bem. O cheiro é o gatilho de investigação.

Duplicated Code (código duplicado)

O cheiro mais citado e talvez o mais incompreendido. A intuição "don't repeat yourself" (DRY) é correta, mas a aplicação cega leva a problemas próprios. Há duas formas de duplicação que parecem iguais e são radicalmente diferentes:

A regra prática de Sandi Metz é poderosa aqui: "duplication is far cheaper than the wrong abstraction". Esperar até a terceira ocorrência (regra dos três, do conceito 01) antes de abstrair é mais seguro que abstrair na segunda e errar a forma da generalização. Quando estiver inseguro, deixe o código duplicado mais um pouco; o padrão real fica mais claro com mais dados.

Feature Envy

Um método que parece mais interessado nos dados de outra classe do que nos dados da sua própria classe. Você lê o método e vê customer.getName(), customer.getAddress(), customer.getEmail()... e quase nada da classe onde o método está. Esse método quer ser amigo de Customer, não de quem o está hospedando.

O refactoring nomeado é Move Method: o método pertence à outra classe. A correção é trivial mecanicamente; o ganho conceitual é grande, porque revela qual era a fronteira correta entre as duas classes — fronteira essa que estava errada antes. Cheiros como Feature Envy são valiosos exatamente porque tornam visíveis decisões mal tomadas anteriormente.

Primitive Obsession (obsessão por primitivos)

Usar string para representar e-mail, CPF, telefone, código de pedido. Usar decimal para representar dinheiro, sem distinguir moeda. Usar int para representar IDs de coisas diferentes que acidentalmente têm o mesmo tipo numérico. O resultado: validações repetidas em todo lugar onde o valor é usado, parâmetros que poderiam ser confundidos (process(int orderId, int userId) — alguém vai trocar a ordem), e regras de negócio espalhadas pelo sistema.

O remédio é Replace Primitive with Object ou, em linguagens com suporte adequado, value objects (records em C# 10+, dataclasses frozen em Python, structs em Go). Um Email que valida no construtor garante que toda instância no sistema é um e-mail válido. Um Money que carrega moeda evita somas absurdas como 10 USD + 5 BRL = 15. Um OrderId tipado evita o bug clássico de passar parâmetros na ordem errada.

Switch Statements

Switches grandes que selecionam comportamento por tipo são quase sempre candidatos a polimorfismo. O sintoma típico é o mesmo switch repetido em vários lugares: cada vez que um novo caso é adicionado, é preciso lembrar de atualizar todos os switches espalhados. Isso é violação dupla — de OCP (cada novo caso modifica código existente) e de coesão (a mesma decisão está espalhada).

O refactoring é Replace Conditional with Polymorphism: cada caso do switch vira uma classe (ou função, ou tipo algébrico) que sabe responder à mesma pergunta de forma específica. Adicionar um novo tipo é adicionar uma classe; o código existente não muda. Em linguagens funcionais ou com tipos algébricos (Rust, Kotlin sealed classes, F# discriminated unions), o compilador ainda garante exhaustividade — você não consegue esquecer um caso.

Shotgun Surgery (cirurgia com escopeta)

O sintoma é familiar: você precisa fazer uma mudança aparentemente pequena, e ela exige tocar em quinze arquivos. "Adicionar um campo a Customer" envolve mexer no DAO, no DTO, no Validator, no Mapper, no Form, no Test, no Migration, no Documentation... O nome é evocativo: cada mudança espalha tiros pelo sistema. A causa é quase sempre baixa coesão — uma mesma "coisa" está representada em pedaços, e mudanças nela exigem coordenação manual.

O remédio é Move Method e Move Field consolidando a "coisa" num lugar único, ou Inline Class dissolvendo classes que eram só camadas de redirecionamento. Sintomatologicamente, Shotgun Surgery é o oposto de Divergent Change (que veremos a seguir): em SS, uma mudança força muitos lugares; em DC, um lugar muda por muitas razões.

Divergent Change (mudança divergente)

O par dialético do anterior. Um arquivo é tocado por todos os tipos de mudança no sistema — hoje porque mudou regra de negócio, amanhã porque mudou formato de log, depois de amanhã porque mudou banco. Cada mudança vem por uma razão diferente, e todas convergem na mesma classe. Isso é violação direta de SRP: a classe tem múltiplas razões para mudar, múltiplos stakeholders. O remédio é separar em classes diferentes, cada uma responsiva a uma cadência de mudança.

Comments (comentários)

Este é controverso, mas vale o desconforto. Fowler classifica comentários como cheiro quando substituem nomes ruins ou explicam código que poderia ser claro. Um comentário que diz "// calcula o desconto progressivo" em cima de um método chamado doStuff() é tentativa de remediar nome ruim — o refactoring real é Rename Method. Um comentário que explica por que um if labiríntico foi escrito daquele jeito é tentativa de remediar lógica obscura — o refactoring real é simplificar a lógica.

Comentários úteis existem: explicar por quê uma decisão estranha foi tomada (especialmente quando contradiz expectativa), referenciar bug reports ou tickets que motivaram código contra-intuitivo, marcar TODOs com prazo. Comentários como esses não são cheiro. Comentários que duplicam o código em prosa são — porque vão divergir com o tempo, e divergência silenciosa entre comentário e código é fonte abundante de bugs.

O que é refatoração

A definição precisa de Fowler é: refatoração é mudar a estrutura interna do código sem alterar seu comportamento externo. Os dois lados são igualmente importantes. "Sem alterar comportamento" significa que todos os testes continuam passando — não há mudança funcional sob a capa de refatoração. "Mudar estrutura interna" significa que algo mudou de fato — não é só commit de "format" ou "rename". Refatoração é cirurgia estrutural com bisturi de precisão, não tapa-buraco.

Essa precisão importa porque, em codebases reais, a palavra "refatoração" é usada para qualquer coisa: às vezes para grandes reescritas (que não são refatoração — são reescritas), às vezes para mudanças funcionais misturadas ("refatorei e adicionei a feature X" é confissão de problema). Manter a definição estrita protege a prática: você sabe que pode merger uma refatoração com confiança porque, se ela é refatoração, comportamento não mudou.

Como refatorar com segurança

O método de Fowler tem cinco regras operacionais que valem internalizar:

  1. Tenha testes antes. Sem testes, "refatoração" é só edição com fé. Os testes são a rede que detecta se você quebrou algo. Se a área a refatorar não tem testes, escreva-os primeiro — chamado characterization tests (testes que documentam o comportamento atual, mesmo que esse comportamento não seja desejável).
  2. Faça passos pequenos. Cada refactoring é uma transformação nomeada e pequena. Extract Method, Rename Variable, Move Method. Composição de pequenos passos chega longe sem nunca quebrar a árvore.
  3. Rode testes a cada passo. Não acumule cinco refactorings antes de rodar. Refatorou, testou. Se quebrou, desfaz e tenta diferente. Esse ciclo curto é o que torna refatoração de baixa-ansiedade.
  4. Commits por refactoring. Um commit por transformação nomeada. Histórico vira documentação: revisor consegue ver a sequência de decisões. Bisect funciona se algo quebrar mais tarde.
  5. Separe refatoração de mudança de comportamento. Nunca misture os dois no mesmo commit ou PR. Se você precisa adicionar feature, primeiro refatore para que a feature fique fácil, depois adicione a feature. Em palavras de Kent Beck: "first make the change easy, then make the easy change".

Os refactorings que você vai usar mais

O catálogo de Fowler tem mais de oitenta refactorings nomeados. Você provavelmente vai usar uma fração pequena no dia a dia, e vale conhecer os mais comuns pelo nome — porque IDE modernas (IntelliJ, Rider, VS Code com extensões) implementam várias deles automaticamente, e usar a IDE para executar é mais seguro que fazer manualmente.

O mesmo cheiro, em três linguagens

O exemplo abaixo mostra Primitive Obsession sendo curado com Value Object nas três linguagens da formação. O cheiro: representar e-mail como string, com validação espalhada. A cura: criar um tipo específico que valida no construtor e impossibilita instâncias inválidas.

C#
// ANTES — Primitive Obsession
public class User {
    public string Email { get; set; }   // qualquer string vale
}

void Notify(string email, string msg) {
    if (!email.Contains("@")) throw new ArgumentException();
    // ... validação repetida em todo lugar
}

// DEPOIS — Value Object com record (.NET 10)
public readonly record struct Email {
    public string Value { get; }

    public Email(string value) {
        if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
            throw new ArgumentException("Invalid email", nameof(value));
        Value = value.ToLowerInvariant();
    }

    public override string ToString() => Value;
}

public class User {
    public Email Email { get; }   // só aceita Email válido
    public User(Email email) => Email = email;
}

void Notify(Email email, string msg) {
    // sem validação aqui — Email já é garantidamente válido
}

readonly record struct dá value semantics, igualdade estrutural e imutabilidade. C# 10+ favorece records para value objects.

Python
# ANTES — Primitive Obsession
@dataclass
class User:
    email: str   # qualquer string vale

def notify(email: str, msg: str) -> None:
    if "@" not in email:
        raise ValueError("invalid email")
    # ... validação repetida

# DEPOIS — Value Object com dataclass frozen
@dataclass(frozen=True)
class Email:
    value: str

    def __post_init__(self):
        if not self.value or "@" not in self.value:
            raise ValueError(f"Invalid email: {self.value!r}")
        # frozen=True impede mutação; precisa de truque para
        # normalizar:
        object.__setattr__(self, "value", self.value.lower())

    def __str__(self) -> str:
        return self.value

@dataclass
class User:
    email: Email   # só aceita Email válido

def notify(email: Email, msg: str) -> None:
    pass  # sem validação — Email é garantido

frozen=True dá imutabilidade. __post_init__ é onde validação acontece. Padrão idiomático em Python 3.10+.

Go
// ANTES — Primitive Obsession
type User struct {
    Email string   // qualquer string vale
}

func Notify(email, msg string) error {
    if !strings.Contains(email, "@") {
        return errors.New("invalid email")
    }
    return nil // ... validação repetida
}

// DEPOIS — Value Object com construtor
type Email struct {
    value string
}

func NewEmail(s string) (Email, error) {
    s = strings.TrimSpace(strings.ToLower(s))
    if s == "" || !strings.Contains(s, "@") {
        return Email{}, fmt.Errorf("invalid email: %q", s)
    }
    return Email{value: s}, nil
}

func (e Email) String() string { return e.value }

type User struct {
    Email Email   // só aceita Email válido
}

func Notify(email Email, msg string) error {
    return nil // sem validação — Email é garantido
}

Go não tem "constructors" — é convenção usar NewXxx. Campo minúsculo (value) garante que só o construtor pode criar instâncias válidas — package-level encapsulation.

Note como cada linguagem expressa o mesmo padrão de forma idiomática diferente, mas o ganho conceitual é o mesmo: depois do refactoring, é impossível ter um Email inválido no sistema. A validação aconteceu no único ponto onde objetos podem nascer; tudo o que vem depois assume o tipo já correto. Isso elimina dezenas de linhas de validação espalhada e remove uma classe inteira de bugs.

A regra do escoteiro como prática diária

Refatoração de larga escala — pegar uma classe gigante e reescrever em duas semanas — é dramática e raramente justificada. Refatoração contínua, em pequenos passos, integrada ao trabalho diário, é o que mantém sistemas saudáveis ao longo de anos. A "regra do escoteiro" — "deixe o acampamento mais limpo do que encontrou" — é a versão prática disso. Toda vez que você toca num arquivo para uma feature ou bug, faça pequenas melhorias enquanto está ali: renomeie uma variável obscura, extraia uma sub-função óbvia, apague um comentário desatualizado, simplifique uma condição.

Cinco minutos por commit. Multiplicado por cem commits por mês, por dez pessoas no time, durante um ano: são milhares de pequenas melhorias acumuladas. Codebases assim não viram legado podre — viram limpos organicamente. Codebases sem essa cultura, mesmo bem projetadas no início, acabam virando o pântano que ninguém quer tocar.

A condição para que isso funcione é a mesma de tudo até aqui: testes que protegem a refatoração. Sem rede de segurança, "limpar" vira gambiarra com risco. Por isso testabilidade (conceito 04) precede este conceito final — eles são interdependentes. Code smells dão o vocabulário; refatorings dão as ferramentas; testes dão a confiança. Os três juntos formam a prática profissional de manutenção de código.

heurística do sênior

Quando estiver lendo código alheio (em PRs ou em codebase desconhecido), anote mentalmente os cheiros que vê — sem necessariamente apontá-los. O ato de nomear treina o olho. Depois de seis meses fazendo isso consistentemente, você passa a ver problemas estruturais antes mesmo de identificar conscientemente o que está vendo. Essa é a intuição que Beck chamou de "smell" — não é dom, é prática acumulada.

Como praticar — fechamento do Módulo 00

Três exercícios fecham bem este módulo:

  1. Auditoria de código próprio antigo. Pegue um repo seu de seis meses ou um ano atrás. Abra cinco arquivos aleatórios. Para cada um, liste os cheiros que reconhece, e ao lado, o refactoring nomeado que aplicaria. Não execute nenhum — só catalogue. Em uma hora, você vai ter feito mais diagnóstico de design do que muita gente faz num ano.
  2. Refactoring kata. Os "katas de refatoração" do Emily Bache (disponíveis no GitHub: SupermarketReceipt, TennisRefactoring, GildedRose) são exercícios pequenos com código intencionalmente ruim e suíte de testes pronta. Você aplica refactorings em sequência, testes verdes a cada passo, e ao fim tem um código limpo. Faça pelo menos um nas três linguagens.
  3. Aplique no projeto deste módulo. O URL Shortener, depois de implementado nas três linguagens, certamente terá cheiros — é normal. Volte nele com olho de auditor: identifique o que mudaria, aplique pelo menos cinco refactorings nomeados, e documente cada um no commit. O git log do refactoring vai ser, ele próprio, material de estudo.

O Módulo 00 fecha aqui. Você sai dele com vocabulário compartilhado para discutir design (SOLID, coesão, acoplamento, composição, testabilidade, cheiros, refactorings nomeados) e com prática nas três linguagens. O próximo módulo (Fundamentos da Engenharia II — Sistema) muda de registro: desce para o que está embaixo do código — sistema operacional, rede, Git como ferramenta de pensamento. Esses fundamentos são pré-requisito para tudo que vem depois (concorrência, distribuídos, observabilidade).

Referências para aprofundar

  1. livro Refactoring: Improving the Design of Existing Code (2nd ed.) — Martin Fowler & Kent Beck (2018). A fonte. Catálogo completo de cheiros e refactorings, exemplos em JavaScript (segunda ed.). Releia ao longo da carreira — sempre acrescenta.
  2. livro Working Effectively with Legacy Code — Michael Feathers (2004). O livro definitivo sobre refatorar código sem testes. Define "characterization tests" e técnicas para introduzir testabilidade em código bloqueado.
  3. livro 99 Bottles of OOP (2nd ed.) — Sandi Metz, Katrina Owen, TJ Stankus (2020). Refatoração ao vivo de um problema simples para mostrar como decisões pequenas afetam design. Em Ruby, conceitualmente universal.
  4. livro Clean Code — Robert C. Martin (2008). Capítulo 17 (Smells and Heuristics) é um catálogo paralelo ao de Fowler, com ênfase em prática de revisão. Útil como segunda leitura.
  5. artigo The Refactoring Catalog (online) — Martin Fowler. refactoring.com — versão navegável do catálogo, gratuita. Use como referência rápida durante refatorações.
  6. artigo The Wrong Abstraction — Sandi Metz (2016). sandimetz.com/blog/2016/1/20/the-wrong-abstraction — defende que duplicação é mais barata que abstração errada. Texto curto, formativo.
  7. artigo Code Smell — Martin Fowler. martinfowler.com/bliki/CodeSmell.html — origem da terminologia, com a história da conversa entre Fowler e Beck. Vale pelo contexto.
  8. artigo Make the Change Easy, Then Make the Easy Change — Kent Beck. tidyfirst.substack.com — Beck destrincha a heurística que orienta refactoring antes de feature. Princípio fundamental.
  9. docs JetBrains Refactoring Reference. jetbrains.com/help/idea/refactoring-source-code.html — IDE com mais refactorings automatizados implementados. Aprenda os atalhos: economizam horas.
  10. docs VS Code Refactoring. code.visualstudio.com/docs/editor/refactoring — refactorings via Code Actions. Integração com extensões de cada linguagem amplia o catálogo.
  11. vídeo All the Little Things — Sandi Metz (RailsConf 2014). YouTube. Refactoring ao vivo de Gilded Rose. 35 min de aula prática sobre os menores passos possíveis com a maior cautela.
  12. vídeo Refactoring Kata: Tennis — Emily Bache. YouTube + GitHub (emilybache/Tennis-Refactoring-Kata). Exercício canônico, em todas as linguagens. Faça nas três da formação.
  13. vídeo Tidy First? — Kent Beck (2023). YouTube. Beck discute quando refatorar antes vs depois. Versão moderna e refinada do método. Material recente.