Anti-Patterns: evite problemas conhecidos
Todo mundo já escreveu código ruim, eu muitas vezes. A primeira versão costuma ser a mais tenebrosa: é ela que sai quando você resolve um problema pela primeira vez, com prazo correndo, sem saber ainda o que aquela escolha vai custar daqui a seis meses.
Boa parte desses problemas já foi catalogada. Quando a mesma solução ruim reaparece em projetos diferentes, times diferentes e linguagens diferentes, alguém dá um nome a ela. Com o nome, dá pra reconhecer o problema antes de escrevê-lo de novo.
Anti-patterns são esses nomes. São soluções que parecem razoáveis na hora de escrever e criam trabalho depois, na hora de ler, mudar ou confiar no que está lá. Este post mostra um exemplo inteiro do espaguete, lista os que aparecem com frequência em projeto real e reescreve o mesmo código com as escolhas trocadas.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| Anti-pattern (antipadrão) | Solução que resolve o problema imediato e cria outros maiores depois. Recebe um nome porque se repete em muitos projetos. |
| Código espaguete (spaghetti code) | Código sem separação visível entre as partes. O fluxo entra e sai de qualquer lugar, e não dá pra ler de cima pra baixo. |
| Retorno antecipado (early return) | Sair da função assim que a condição inválida aparece, em vez de aninhar o resto dentro de um if. A verificação que faz isso logo na entrada é a cláusula de proteção (guard clause). |
| Step down rule (regra de descida) | Ordenar as funções de cima pra baixo: o orquestrador primeiro, cada auxiliar logo abaixo de quem a chama. Como são irmãs no módulo, cada uma se testa isolada. |
| Densidade visual (visual density) | Uso de linhas em branco pra separar grupos de instruções, de forma que você ache o começo de cada passo sem reler o bloco. |
| Efeito colateral (side effect) | Tudo que a função faz além de calcular e devolver: gravar no banco, escrever log, disparar e-mail, alterar um objeto que veio de fora. |
| Contrato de retorno | O acordo sobre o que a função devolve. Um contrato claro devolve sempre o mesmo formato. |
| Result (resultado estruturado) | Objeto que devolve o desfecho da operação junto com o motivo, no lugar de um true/false solto. |
| Mutação (mutation) | Alterar um objeto que já existe, em vez de produzir um novo com a alteração aplicada. |
| Determinismo | Mesma entrada, mesma saída, sempre. Um Math.random() no meio de uma regra de negócio quebra isso. |
| Código morto (dead code) | Trecho que ninguém chama, ou que nunca é alcançado. |
Um exemplo inteiro: código espaguete
O código espaguete é o anti-pattern mais citado, e o mais fácil de reconhecer depois que você viu um. Não tem estrutura, não tem contrato, não tem intenção visível. O código funciona, e mudar uma linha dele exige reler o arquivo inteiro pra saber o que mais deixa de funcionar.
❌ Deixa eu ver esse espaguetinho…
// Exemplo propositalmente RUIM
// Código todo misturado, sem conceitos claros
realizaVenda(123);
function realizaVenda(x) {
let resultado;
// nomes ruins
let p = buscaPedido(x);
if (p != null) {
if (p.itens && p.itens.length > 0) {
if (!p.c.inadimplente) {
// começa a fazer um monte de coisa no meio
if (p.total > 100) {
p.desconto = 10;
} else {
p.desconto = 0;
}
apply(p);
function apply(p) {
if (p.desconto) {
p.total = p.total - p.desconto;
}
}
let salvo = salvaPedido(p);
if (salvo) {
resultado = salvo;
// lógica aleatória no meio
if (Math.random() > 0.5) {
console.log("Log qualquer");
} else {
console.warn("Outro log");
}
} else {
resultado = null;
}
} else {
// mais lógica aninhada com função no meio
notify(detalhesDoPedido);
resultado = false;
// nomes ruins e mistura de português com inglês
function notify(p) {
console.log("cliente inadimplente", p?.cliente?.nome);
return true;
}
}
} else {
resultado = undefined;
}
} else {
resultado = null;
}
// código morto
if (false) {
console.log("nunca executa");
}
return resultado;
function salva(p) {
if (!p) return;
if (p.total < 0) return null;
return { ...p, salvo: true };
}
}Pra chegar na regra do desconto, você entra em vários if's. Pra descobrir o que a função devolve, precisa achar as cinco atribuições de resultado espalhadas pelos if e simular, uma a uma, qual delas vale pro pedido que você tem em mãos. A função salva no rodapé não é chamada por ninguém: quem grava o pedido é salvaPedido, que nem está definida ali. E o Math.random() escolhe qual log sai, então a mesma venda produz saídas diferentes a cada execução. 😅
O que torna esse código caro
Os problemas abaixo estão todos naquele arquivo. Cada linha da tabela mostra o que aparece no exemplo e o que entra no lugar.
Nomes e legibilidade
| No exemplo acima | O que colocar no lugar |
|---|---|
x, p, c, apply: letras soltas que só quem escreveu decifra | codigoDoPedido, detalhesDoPedido: o nome diz o que a variável guarda |
realizaVenda em português e notify, apply em inglês, no mesmo arquivo | Um idioma só, do começo ao fim |
| O nome não conta a intenção, então o comentário acima dele precisa contar | O nome conta a intenção, e o comentário deixa de ser necessário |
Controle de fluxo
| No exemplo acima | O que colocar no lugar |
|---|---|
if dentro de if dentro de if: a regra de negócio está no quarto nível de indentação | Retorno antecipado na entrada, e o caminho de sucesso fica no primeiro nível |
p != null compara com coerção implícita, tratando null e undefined como iguais | === e !== comparam sem conversão, e o que você lê é o que acontece |
| A decisão da venda está repartida entre três blocos distantes | Uma cláusula de proteção por decisão, na ordem em que elas acontecem |
Retorno e contrato
| No exemplo acima | O que colocar no lugar |
|---|---|
A mesma função devolve null, undefined, false ou um objeto, conforme o caminho | Um formato só, em todos os caminhos |
| Quem chama testa nulo a cada passo pra descobrir o que aconteceu | Um Result com status e motivo responde isso sem adivinhação |
false não diz por que a venda não saiu | O motivo vem junto do desfecho: CLIENTE_INADIMPLENTE |
Estrutura e design
| No exemplo acima | O que colocar no lugar |
|---|---|
| Uma função busca, valida, calcula desconto, grava e escreve log | Uma responsabilidade por função, um nível de abstração por vez |
| Regra de negócio, gravação e log no mesmo bloco | Domínio separado da persistência e do efeito colateral |
apply, notify e salva declaradas no meio do fluxo, longe de quem chama | Auxiliares logo abaixo de quem as usa, na ordem de chamada |
salva não é chamada por ninguém, e o if (false) nunca executa | Apague no mesmo commit em que encontrar. O histórico do git preserva. |
Estado e mutabilidade
| No exemplo acima | O que colocar no lugar |
|---|---|
let resultado recebe valor em cinco pontos, e o final depende do caminho percorrido | const por padrão: cada valor é atribuído uma vez, onde você o lê |
p.total e p.desconto são alterados dentro de uma função que recebeu p de fora | A função devolve o pedido com desconto aplicado, e quem chamou decide o que fazer |
p.c.inadimplente: pra saber o que é c, você abre buscaPedido | detalhesDoPedido.cliente.inadimplente: o caminho se lê sem sair do arquivo |
Efeitos colaterais e previsibilidade
| No exemplo acima | O que colocar no lugar |
|---|---|
console.log e console.warn no meio do cálculo | Efeito colateral com nome e lugar próprios: notificaInadimplencia |
Math.random() escolhe o log, então a mesma venda dá saídas diferentes | Determinismo na regra: mesma entrada, mesma saída |
| Testar a função exige executar o que ela grava e imprime | Cálculo separado do efeito, e o teste chama só o cálculo |
Esses problemas raramente aparecem sozinhos, e um facilita o outro. Com let resultado declarado no topo, tratar mais um caso custa uma linha, então o próximo if entra sem resistência. Com quatro níveis de indentação já no arquivo, introduzir um retorno antecipado exige desmontar os quatro, e quem está com prazo escreve o quinto.
Outros anti-patterns comuns em projetos reais
Fora do espaguete, existem padrões que passam na revisão porque cada um deles, isolado, parece razoável.
Modelagem e contrato de dados
| O padrão ruim | O que colocar no lugar |
|---|---|
true/false puro como resposta de uma operação de negócio | Result estruturado: o desfecho mais o motivo |
| Tipos de retorno diferentes na mesma função | Um formato só |
Objeto de resultado com campos vazios pra manter a forma (meta: {}, data: {}) | Só os campos que aquele desfecho preenche |
Result carregando statusCode de HTTP (HyperText Transfer Protocol · o protocolo da web) | A regra devolve o desfecho, e a borda traduz esse desfecho em status HTTP |
| Retorno de objeto anônimo, montado diferente em cada chamada | Um formato nomeado, igual em todas |
UI acoplada ao que não é dela
| O padrão ruim | O que colocar no lugar |
|---|---|
fetch (chamada de rede do navegador) escrito dentro do componente | A chamada fica em um apiClient, o ponto único que conversa com o servidor |
| Chamadas à API espalhadas, cada uma com sua própria configuração | Um apiClient só, com a URL base, os cabeçalhos e o tratamento de erro |
Regra de negócio dentro de useEffect (gancho do React que roda depois da renderização) | A regra fica no servidor ou em módulo próprio. O componente exibe. |
| Dados brutos chegando na tela e sendo transformados ali | Os dados chegam no formato que a tela desenha |
try/catch repetido em toda tela | Erro tratado na borda, uma vez |
| Várias fontes de verdade pro mesmo dado no state (estado da interface) | Uma fonte de verdade, e o resto deriva dela |
Design de código
| O padrão ruim | O que colocar no lugar |
|---|---|
Nomes genéricos: handle, process, manage | Verbo que diz o que a função faz: calculaTotal, emiteNotaFiscal |
| Função com mais de uma responsabilidade clara | Uma responsabilidade por função, e o nome cabe sem “e” no meio |
| A mesma estrutura duplicada entre camadas | Regra de três: extraia na terceira ocorrência, não na primeira |
| Cache (memória de resposta pronta) misturado com a regra de negócio | Cache na borda, em volta da regra. A regra é escrita sem citar o cache. |
| Abstração criada pra um caso que ainda não existe | Escreva quando o segundo caso chegar |
Fluxo e controle
| O padrão ruim | O que colocar no lugar |
|---|---|
| Exception como fluxo normal, por exemplo lançar erro pra sinalizar “não encontrei” | null explícito ou Result. Exception fica pro que é erro de verdade. |
| Aninhamento onde caberia um retorno antecipado | Cláusula de proteção na entrada |
| Código morto e trechos inalcançáveis | Apague |
O mesmo código, reescrito
As escolhas são as da coluna da direita: orquestrador no topo, auxiliares abaixo pela regra de descida (step down rule), densidade visual e nomes que dizem a intenção.
✅ Deixa eu ver o código narrativo…
// ✅ Código narrativo
// Orquestrador no topo, auxiliares abaixo pela regra de descida (step down),
// densidade visual e nomes expressivos.
import { buscaPedido, salvaPedido } from "pedidos/repositorio.js";
import { notificaInadimplencia } from "notificacoes/inadimplencia.js";
import { aplicaDescontos } from "pedidos/desconto.js";
const VENDA_APROVADA = "APROVADA";
const VENDA_RECUSADA = "RECUSADA";
const venda = await realizaVenda(123);
async function realizaVenda(codigoDoPedido) {
const detalhesDoPedido = await buscaPedido(codigoDoPedido);
if (!temItensParaFaturar(detalhesDoPedido)) {
const vendaSemItens = recusaVenda("PEDIDO_SEM_ITENS");
return vendaSemItens;
}
if (detalhesDoPedido.cliente.inadimplente) {
notificaInadimplencia(detalhesDoPedido);
const vendaBloqueada = recusaVenda("CLIENTE_INADIMPLENTE");
return vendaBloqueada;
}
const notaFiscal = await emiteNotaFiscal(detalhesDoPedido);
const vendaConcluida = aprovaVenda(notaFiscal);
return vendaConcluida;
}
function temItensParaFaturar(pedido) {
const itens = pedido?.itens ?? [];
const temAlgumItem = itens.length > 0;
return temAlgumItem;
}
function recusaVenda(motivo) {
const venda = { status: VENDA_RECUSADA, motivo, notaFiscal: null };
return venda;
}
async function emiteNotaFiscal(pedido) {
const pedidoComDesconto = aplicaDescontos(pedido);
const notaFiscal = await salvaPedido(pedidoComDesconto);
return notaFiscal;
}
function aprovaVenda(notaFiscal) {
const venda = { status: VENDA_APROVADA, motivo: null, notaFiscal };
return venda;
}O fluxo principal são quatro passos, lidos de cima pra baixo: busca o pedido, recusa se não tem item, recusa se o cliente está inadimplente, fatura. Cada return tem uma const nomeada logo acima, então o nome diz qual dos três desfechos é aquele.
O contrato agora é um só. Nos três caminhos, a função devolve um objeto com status, motivo e notaFiscal. Quem chama lê venda.status e sabe o que aconteceu, e no caso da recusa o motivo diz qual das duas regras barrou a venda. Compare com o exemplo anterior, onde false e undefined significavam coisas diferentes e nenhum dos dois explicava o quê.
As auxiliares ficam abaixo do orquestrador, cada uma logo depois de onde é chamada, e você só desce até elas se quiser o detalhe. É a regra de descida: a leitura vai do passo para o detalhe, de cima para baixo. Como são funções irmãs, e não presas dentro de realizaVenda, cada uma se testa isolada, e quando outra venda precisar barrar um pedido, recusaVenda já está lá pra reusar. aplicaDescontos devolve o pedido com desconto em vez de alterar o que recebeu, então nenhuma função altera objeto que veio de fora.
Um idioma só, do começo ao fim
O código acima está em português porque o exemplo ruim também está, e assim os dois se comparam linha a linha. Fora de um post, eu escreveria os dois em inglês.
O defeito do exemplo ruim nunca foi o português. Foi a mistura dos dois: realizaVenda e buscaPedido estão no mesmo arquivo que apply e notify, e uma função chamada salva fica logo abaixo de outra chamada salvaPedido. Chamam isso de portuglês, e o custo aparece toda vez que você vai chamar alguma coisa: era notify ou notifica? O autocomplete do editor não resolve, porque ele lista os dois. Você abre o arquivo pra conferir, e repete isso a cada nome do projeto.
As duas línguas se defendem, e eu recomendo o inglês por um motivo prático: metade do arquivo já está em inglês e isso não muda. if, return, length, console.log e Math.random vêm da linguagem. Com os nomes em inglês, a linha inteira se lê de uma vez, sem trocar de idioma no meio. É também o idioma da documentação que você vai consultar, da mensagem de erro que vai colar no buscador e da biblioteca que vai instalar.
Abaixo, o mesmo código, com essa convenção aplicada. É a versão que eu recomendo.
🌎 Deixa eu ver a versão recomendada, em inglês…
// ✅ Recomendado: um idioma só, do começo ao fim.
// Mesma estrutura do exemplo anterior, nomes em inglês.
import { findOrder, saveOrder } from "orders/repository.js";
import { notifyOverdueCustomer } from "notifications/overdue.js";
import { applyDiscounts } from "orders/discount.js";
const SALE_APPROVED = "APPROVED";
const SALE_REJECTED = "REJECTED";
const sale = await sellOrder(123);
async function sellOrder(orderId) {
const orderDetails = await findOrder(orderId);
if (!hasBillableItems(orderDetails)) {
const saleWithoutItems = rejectSale("ORDER_WITHOUT_ITEMS");
return saleWithoutItems;
}
if (orderDetails.customer.isOverdue) {
notifyOverdueCustomer(orderDetails);
const blockedSale = rejectSale("CUSTOMER_OVERDUE");
return blockedSale;
}
const taxInvoice = await issueTaxInvoice(orderDetails);
const completedSale = approveSale(taxInvoice);
return completedSale;
}
function hasBillableItems(order) {
const items = order?.items ?? [];
const hasAnyItem = items.length > 0;
return hasAnyItem;
}
function rejectSale(reason) {
const rejectedSale = { status: SALE_REJECTED, reason, taxInvoice: null };
return rejectedSale;
}
async function issueTaxInvoice(order) {
const discountedOrder = applyDiscounts(order);
const taxInvoice = await saveOrder(discountedOrder);
return taxInvoice;
}
function approveSale(taxInvoice) {
const approvedSale = { status: SALE_APPROVED, reason: null, taxInvoice };
return approvedSale;
}A estrutura é idêntica à versão em português, e é isso que a comparação mostra: trocar o idioma não conserta nem estraga nada do que as tabelas apontaram. São dois assuntos separados.
O idioma único vale para o código: nomes de variável, função e constante. Já as mensagens, rótulos, tags e botões que aparecem na tela ficam na língua de quem usa o sistema, porque existem pra ajudar essa pessoa. CUSTOMER_OVERDUE é um código interno em inglês, e a frase que o cliente vê sai traduzida na borda: “Não foi possível concluir a compra: há faturas em aberto.”
Um nome do exemplo merece explicação: nota fiscal virou taxInvoice, e não invoice. São dois documentos diferentes, e vale conhecer a distinção antes de escolher a palavra:
- Fatura é o documento comercial que detalha o que foi vendido e cobra o valor. Não tem validade tributária e não é obrigatória por lei. Em inglês, é ela que se chama
invoice. - Nota fiscal é o documento fiscal obrigatório, que registra a operação para o fisco e comprova o recolhimento do imposto.
Hoje o mais comum é a NF-e trazer a fatura e suas parcelas dentro da própria nota, no grupo de cobrança. Pela Lei das Duplicatas (Lei 5.474/1968, art. 1º) é o inverso: a fatura fica por cima e reúne várias notas numa cobrança só.
Em um sistema que emite nota fiscal e fatura, usar invoice para representar a nota fiscal ocupa um nome que faz mais sentido para a fatura. Quando ela for adicionada ao sistema, será preciso recorrer a nomes menos claros, como invoice2 ou billingInvoice. Por isso, no exemplo acima, foi utilizado taxInvoice. Essa é uma sugestão, não uma regra: se o sistema trabalha apenas com um desses documentos, invoice é um nome perfeitamente adequado.
Quem decide isso é o time, e a decisão depende do contexto do projeto. O que ajuda em qualquer contexto é deixar a escolha registrada num glossário. No Simula Impostos, um app que escrevi pra comparar encargos tributários, o README traz um: os identificadores estão em inglês, a interface que o contador usa está em português, e a tabela liga os dois.
| Código (EN) | Interface (PT-BR) |
|---|---|
invoice | Nota Fiscal |
issuer | Emitente |
recipient | Destinatário |
taxRegime | Regime Tributário |
totalInvoice | Valor Total da NF |
taxId | CNPJ / CPF |
São cerca de 30 linhas no README do projeto. O contador pede uma mudança no “Emitente”, e você sabe que o campo é issuer sem abrir o código pra conferir. Quem entra no projeto lê o glossário uma vez, em vez de descobrir o padrão por tentativa e acrescentar um terceiro nome.
É o que o Code Style que sigo tenta resolver: não qual convenção é a certa, mas onde ela está anotada.
As siglas vêm depois
Existem várias siglas em volta desse assunto:
- DDD (Domain-Driven Design · modelar o código na linguagem do negócio)
- TDD (Test-Driven Development · escrever o teste antes do código)
- SOLID (cinco princípios de design orientado a objetos)
- YAGNI (You Aren’t Gonna Need It · não construa o que ainda não pediram)
Todas ajudam, e nenhuma delas é o ponto de partida. O ponto de partida é escrever código que a próxima pessoa consiga ler. As siglas dão nome próprio a hábitos que você já vai estar praticando quando chegar nelas.
Se o código que você mantém hoje já tem vários desses padrões, a saída não precisa ser reescrever tudo. Cada um deles entrou por um commit separado, e sai do mesmo jeito: na próxima vez que abrir aquele arquivo pra mudar uma regra, corrija um.
Happy coding!
Referências
- Code Style, o guia de convenções que sigo
- Refactoring Guru: Code Smells
- Clean Code, de Robert C. Martin
- SOLID Principles
- YAGNI
Ver todos os artigos →