Fundamentos de Programação/02 - Código que Presta/Semana 06 - Código Limpo e Refatoração9 min
Code Smells
- Método longo, classe deus, feature envy, primitive obsession: reconhece os quatro no seu código?
- Smell é bug? Se não é, por que vale corrigir?
- Qual smell do seu projeto atual mais te custa tempo por semana?
- Quando um smell é aceitável e mexer nele é perda de tempo?
Conceito
Um code smell é um sintoma na estrutura do código que sugere um problema mais profundo. A palavra "cheiro" é deliberada: não é prova, é indício — algo que merece investigação, não correção automática.
Smell não é bug. O código funciona, passa nos testes, está em produção. Então por que corrigir?
Porque smell é dívida cujo juro você paga em toda leitura e em toda alteração. Ele não te custa uma falha; te custa tempo, todas as vezes. E o custo é composto: código difícil de entender é difícil de mudar com segurança, e mudanças inseguras produzem os bugs de verdade.
Os quatro do plano, e o sinal concreto de cada um:
Método longo. O problema real não é o comprimento — é misturar níveis de abstração. O sinal mais confiável: você precisou de comentários para separar seções. Cada comentário-seção é o nome de um método que quer nascer.
Classe deus. Sabe demais e faz demais. Sinais, em ordem de confiabilidade:
- o nome é genérico —
Manager,Helper,Util,Service,Handler - tem campos que não se relacionam entre si
- todo commit toca nela (o sinal mais objetivo, e verificável no
git log)
Feature envy. Um método usa mais dados de outro objeto do que do próprio. Sinal
visual: outro.GetA() + outro.GetB()*outro.GetC(). A correção é mover o método para
onde os dados moram — o método está com inveja da classe errada.
Primitive obsession. Usar tipos primitivos para conceitos de domínio: string para
CPF, int para dinheiro, float para porcentagem, map[string]string para um objeto. O
custo é triplo: validação espalhada por todo lugar que recebe, nenhuma segurança de tipo
(dá para somar CPF com telefone), e unidade ambígua (é reais ou centavos? segundos ou
milissegundos?).
Quando um smell é aceitável:
- código que não vai mudar (script de migração já executado, gerado)
- protótipo com prazo, explicitamente descartável
- quando a correção introduziria 2. Abstração prematura
O critério de priorização é único e simples: quantas vezes esse arquivo muda? Smell em código que muda toda semana custa toda semana. Smell em código estável não custa nada.
Em Go
Anti-nomes. Go tem uma cultura forte de nomeação, e ela é uma ferramenta de detecção de smell:
| Nome | Por que é smell |
|---|---|
util, helper, common, misc |
o propósito é "coisas" — viola SRP por definição |
manager, service, handler sozinhos |
não dizem o que fazem |
base, abstract |
Go não tem herança; o nome é importado de outro paradigma |
pacote models/, pkg/ |
organiza por camada técnica, não por domínio |
A convenção de Go é nomear o pacote pelo que ele oferece: http, json, bytes,
sort. pedido.Criar() lê melhor que services.PedidoService.CreatePedido().
Primitive obsession é mais fácil de cometer em Go porque a linguagem não tem enum e
string é barata. E é mais fácil de corrigir do que na média, porque tipo definido
custa zero em runtime (1. Tipos e Sistema de Tipos):
type CPF string
type Centavos int64
type IDUsuario int64
Mesma representação, custo zero, e o compilador passa a recusar somar CPF com telefone.
Método longo em Go tem um agravante: if err != nil { return ... } infla qualquer
função. A correção não é esconder o erro (nem panic, nem ignorar) — é extrair a
operação inteira para uma função que devolve um erro só.
Smells específicos de Go:
any/interface{}em assinatura pública — perdeu a tipagem- retornar
(T, bool, error)— dois mecanismos de "deu errado" context.Contextque não é o primeiro parâmetro (ou pior: guardado em struct)naked return(returnsem valores, com retornos nomeados) em função longa — o leitor precisa procurar o que está sendo devolvido- função com 6 parâmetros — vira struct de configuração ou functional options
elsedepois dereturn— Go prefera early return
Ferramentas: go vet (correção), golangci-lint (agregador — inclui gocyclo para
complexidade ciclomática, dupl para duplicação, revive para estilo), gofmt (não
negociável e não configurável, o que elimina toda discussão de formatação).
Respostas às perguntas-guia
1. Método longo, classe deus, feature envy, primitive obsession: reconhece os quatro no seu código?
Conceito: os sinais objetivos são: comentários separando seções (método longo), arquivo
que aparece em todo commit (classe deus), cadeia de getters de outro objeto (feature
envy), string/int carregando conceito de domínio (primitive obsession).
Em Go, onde procurar primeiro:
- pacote chamado
utilouhelper - função com mais de ~3 blocos
if err != nilseguidos map[string]anyatravessando a fronteira de um pacotestringcomo ID, CPF, e-mail, ou unidade monetária
2. Smell é bug? Se não é, por que vale corrigir?
Conceito: não é. Vale porque o custo é o custo de mudança, pago em toda leitura e alteração, e composto (código obscuro produz mudanças inseguras, que produzem bugs).
Em Go: há um custo extra concreto — smell de pacote (util) tende a criar dependências
que aumentam o risco de importação circular, que o compilador recusa
(4. Grafos). Em Go, arquitetura ruim vira erro de compilação mais cedo que em outras
linguagens.
3. Qual smell do seu projeto atual mais te custa tempo por semana?
Conceito: mede-se, não se adivinha: cruze a lista de smells com a frequência de alteração dos arquivos.
Em Go, o comando que responde isso:
git log --format= --name-only --since='6 months' | sort | uniq -c | sort -rn | head -20
Os arquivos do topo são onde corrigir smell paga. Se um deles se chama util.go ou tem
1.000 linhas, você achou o alvo da semana.
4. Quando um smell é aceitável e mexer nele é perda de tempo?
Conceito: quando o código não muda, quando é descartável por decisão, ou quando a "correção" seria abstração prematura.
Em Go: código gerado (//go:generate, protobuf, sqlc, mocks) é cheio de smells e
não se toca — ele é regenerado. A convenção é o cabeçalho // Code generated by ... DO NOT EDIT., e o golangci-lint já ignora esses arquivos por padrão.
Trade-offs
Do conceito:
- Corrigir smell compra custo de mudança menor no futuro; cobra tempo agora e o risco de introduzir bug numa mudança que não precisava acontecer.
- Ignorar smell é a decisão certa em código estável e a errada em código quente.
- Catalogar smells dá vocabulário comum e cria o risco de virar checklist aplicado sem julgamento.
Em Go:
- Tipo definido contra primitive obsession custa zero em runtime e cobra conversões explícitas nas bordas (JSON, SQL) — ruído real.
gofmtelimina a discussão de formatação; o custo é que você não escolhe nada.- Linter agressivo pega smell de verdade e também acusa muito falso positivo; a
configuração do
golangci-linté uma decisão de equipe, não um default.
Exemplo prático
Os quatro smells, e a versão corrigida ao lado:
package main
import (
"errors"
"fmt"
"regexp"
"strings"
)
// ==================== ANTES: primitive obsession ====================
// Tudo é string e int. Nada impede trocar cpf por telefone, nem somar
// centavos com reais, nem passar um e-mail no lugar do nome.
type PedidoRuim struct {
ClienteCPF string
ClienteEmail string
Total int // reais? centavos? ninguém sabe
Desconto int
}
func criaPedidoRuim(cpf, email string, total, desc int) (*PedidoRuim, error) {
if len(cpf) != 11 { // validação espalhada por todo chamador
return nil, errors.New("cpf inválido")
}
if !strings.Contains(email, "@") {
return nil, errors.New("email inválido")
}
return &PedidoRuim{cpf, email, total, desc}, nil
}
// ==================== DEPOIS: tipos de domínio ====================
type CPF string
type Email string
type Centavos int64 // a unidade está NO TIPO
var reEmail = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)
func NovoCPF(s string) (CPF, error) {
limpo := strings.Map(func(r rune) rune {
if r >= '0' && r <= '9' {
return r
}
return -1
}, s)
if len(limpo) != 11 {
return "", fmt.Errorf("cpf %q: precisa de 11 dígitos, tem %d", s, len(limpo))
}
return CPF(limpo), nil // validado UMA vez, na fronteira
}
func NovoEmail(s string) (Email, error) {
if !reEmail.MatchString(s) {
return "", fmt.Errorf("email %q inválido", s)
}
return Email(strings.ToLower(s)), nil
}
func (c Centavos) Reais() float64 { return float64(c) / 100 }
func (c Centavos) String() string { return fmt.Sprintf("R$ %.2f", c.Reais()) }
type Cliente struct {
CPF CPF
Email Email
}
type Pedido struct {
Cliente Cliente
Total Centavos
Desconto Centavos
}
// ==================== feature envy: ANTES ====================
// Este método vive em Relatorio mas só usa dados de Pedido.
type RelatorioRuim struct{}
func (RelatorioRuim) ValorFinal(p Pedido) Centavos {
return p.Total - p.Desconto // inveja: só toca dados de Pedido
}
// ==================== feature envy: DEPOIS ====================
// O método mudou de casa: agora mora onde os dados moram.
func (p Pedido) ValorFinal() Centavos { return p.Total - p.Desconto }
// ==================== método longo: ANTES ====================
func processaRuim(cpfStr, emailStr string, total, desc int) (string, error) {
// valida cliente
if len(cpfStr) != 11 {
return "", errors.New("cpf inválido")
}
if !strings.Contains(emailStr, "@") {
return "", errors.New("email inválido")
}
// calcula
final := total - desc
if final < 0 {
final = 0
}
// formata
return fmt.Sprintf("cliente %s paga R$ %.2f", cpfStr, float64(final)/100), nil
}
// ==================== método longo: DEPOIS ====================
// Cada comentário-seção virou uma função com nome. O corpo lê como a lista
// de passos que o comentário descrevia.
func processa(cpfStr, emailStr string, total, desc Centavos) (string, error) {
cliente, err := montaCliente(cpfStr, emailStr)
if err != nil {
return "", fmt.Errorf("processa pedido: %w", err)
}
p := Pedido{Cliente: cliente, Total: total, Desconto: desc}
return p.Descreve(), nil
}
func montaCliente(cpfStr, emailStr string) (Cliente, error) {
cpf, err := NovoCPF(cpfStr)
if err != nil {
return Cliente{}, err
}
email, err := NovoEmail(emailStr)
if err != nil {
return Cliente{}, err
}
return Cliente{cpf, email}, nil
}
func (p Pedido) Descreve() string {
return fmt.Sprintf("cliente %s paga %s", p.Cliente.CPF, p.ValorFinal())
}
func main() {
// primitive obsession: o compilador não te protege de nada
ruim, _ := criaPedidoRuim("12345678901", "a@b.com", 10000, 500)
fmt.Printf("ruim: total=%d desconto=%d (reais? centavos?)\n", ruim.Total, ruim.Desconto)
// tipos de domínio: a unidade e a validação estão no tipo
s, err := processa("123.456.789-01", "ANA@Exemplo.com", 10000, 500)
fmt.Println("bom: ", s, err)
_, err = processa("123", "ana@exemplo.com", 100, 0)
fmt.Println("cpf curto:", err)
_, err = processa("12345678901", "sem-arroba", 100, 0)
fmt.Println("email ruim:", err)
// o compilador agora recusa a troca de conceitos
var cpf CPF = "12345678901"
var email Email = "a@b.com"
// cpf = email // NÃO compila: cannot use email (Email) as CPF
fmt.Println("tipos distintos:", cpf != "", email != "")
// feature envy corrigida: o método mora onde os dados moram
p := Pedido{Total: 10000, Desconto: 1550}
fmt.Println("valor final:", p.ValorFinal(), "| Stringer:", p.Total)
}
Repare no que a correção da primitive obsession entregou de graça: a validação passou a
existir uma vez (em NovoCPF), a unidade ficou documentada no tipo (Centavos),
e a formatação virou responsabilidade do próprio valor (String()). Três smells caíram
com uma mudança.
Relacionado
- 2. Single Responsibility Principle — a classe deus é violação de SRP
- 4. Refatorações Nomeadas — os movimentos que corrigem cada smell
- 1. Tipos e Sistema de Tipos — semana 1, tipo definido custa zero
- 1. Teste Unitário — semana 7, teste é o que torna a correção segura
Parte de Semana 06 - Código Limpo e Refatoração · 00 - MOC Fundamentos de Programação