Fundamentos de Programação/02 - Código que Presta/Semana 06 - Código Limpo e Refatoração10 min
Refatorações Nomeadas
- Extrair método: como escolher o nome sem repetir o corpo do método?
- Extrair classe: qual o sinal de que um grupo de campos quer sair de casa?
- Substituir condicional por polimorfismo: quando isso melhora e quando só espalha a lógica?
- Por que refatoração nomeada permite commit pequeno e revisão fácil?
Conceito
Refatorar é mudar a estrutura sem mudar o comportamento.
A segunda metade é a que define a palavra. Se você não sabe que o comportamento continua o mesmo, você não está refatorando — está reescrevendo, e chamando de refatoração para parecer mais seguro. É por isso que teste é pré-requisito, não complemento (1. Teste Unitário).
Por que dar nome às refatorações? Fowler catalogou dezenas, e o valor do catálogo é triplo:
- vocabulário — "extraia esse método" comunica mais rápido que descrever o movimento
- passos mecânicos — cada refatoração tem uma receita verificável, que reduz a chance de mudar comportamento por acidente
- commit pequeno e nomeado — cada refatoração é um commit que compila e passa, revisável e revertível isoladamente
Extrair método — e o que ninguém explica: como escolher o nome.
O nome deve dizer o quê; o corpo diz como. Se o nome repete o corpo
(incrementaContadorEAtualizaData), você extraiu na fronteira errada. A fronteira certa
é onde existe um conceito — algo que você poderia explicar a um colega sem ler o
código.
O sinal de que a extração está pronta: o método chamador passa a ler como uma lista de passos, no mesmo nível de abstração.
Extrair classe — o sinal é um grupo de campos que sempre aparecem juntos
(rua, numero, cep → Endereco), ou um subconjunto de métodos que só toca um
subconjunto de campos. O segundo é mais confiável: se metade dos métodos nunca toca metade
dos campos, há duas coisas ali.
Substituir condicional por polimorfismo — a única das três que frequentemente piora:
| Melhora quando | Piora quando |
|---|---|
o mesmo switch se repete em vários lugares |
existe um switch, num lugar |
| adicionar um caso exige tocar N arquivos | os casos são fechados e poucos |
| cada caso tem lógica substancial | cada caso é uma linha |
Com um switch de 10 linhas num lugar só, trocar por 5 tipos espalha em 5 arquivos algo
que era legível de uma vez. O critério é: quantos lugares eu toco para adicionar um
caso? Se é um, deixe o switch.
Por que commits pequenos e nomeados: cada um compila, passa nos testes, e tem um
motivo. Isso torna a revisão possível, o revert cirúrgico, e o git bisect capaz de
apontar o commit exato que quebrou (5. Bisect).
Em Go
Ferramentas, e uma que é obrigatória:
| Ferramenta | O que faz |
|---|---|
gofmt / gofumpt |
formata. Não configurável — elimina toda discussão |
gopls rename (ou IDE) |
renomeia com segurança em todo o módulo, respeitando escopo |
gopls "extract function/variable" |
extrai método/variável mecanicamente |
golangci-lint |
detecta os candidatos (complexidade, duplicação, função longa) |
go vet |
erros reais, não estilo |
Extrair método em Go tem um passo intermediário útil: extrair para uma closure local primeiro. Ela captura as variáveis do escopo, então você não precisa decidir a assinatura ainda. Depois de ver o que ela realmente usa, você promove para função ou método com os parâmetros certos. É refatoração em dois passos, cada um trivial.
Substituir condicional por polimorfismo é menos idiomático em Go do que em Java, e o substituto é frequentemente melhor: mapa de funções.
var handlers = map[string]func(Pedido) error{
"criar": criar,
"cancelar": cancelar,
}
Isso te dá extensibilidade (adicionar caso é uma linha) sem criar cinco tipos e cinco
arquivos. Go prefere switch explícito quando os casos são fechados e poucos, e mapa de
funções quando são muitos ou abertos. Hierarquia de tipos é a última opção — e nem
existe herança para sustentá-la (4. Composição vs Herança).
A refatoração mais frequente em Go é uma que não está no catálogo do Fowler: extrair pacote. Porque a unidade de encapsulamento é o pacote (1. Encapsulamento), separar responsabilidades (2. Single Responsibility Principle) quase sempre significa mover tipos para um pacote novo. E o compilador verifica o resultado: se a separação criou dependência circular, é erro de build.
Uma refatoração específica de Go: extrair interface. Você tem um tipo concreto usado por um consumidor; extrai a interface no consumidor com só os métodos usados. O tipo concreto não muda nem uma linha — satisfação implícita (3. Interfaces). É a refatoração de menor risco possível: zero alterações no código existente.
Respostas às perguntas-guia
1. Extrair método: como escolher o nome sem repetir o corpo do método?
Conceito: o nome diz o quê, o corpo diz como. Se o nome enumera as ações, a fronteira está errada — procure o conceito, não a sequência.
Em Go: nomes curtos são idiomáticos, e o pacote já dá contexto. Dentro do pacote
pedido, a função é Cancela, não CancelaPedidoDoBanco. pedido.Cancela() é o nome
completo do ponto de vista de quem chama.
2. Extrair classe: qual o sinal de que um grupo de campos quer sair de casa?
Conceito: campos que sempre aparecem juntos, ou um subconjunto de métodos que só toca um subconjunto de campos.
Em Go: o sinal extra é a lista de parâmetros repetida. Se três funções recebem
(rua, numero, cidade, cep string), esse grupo é um tipo — e em Go a promoção é indolor,
porque uma struct de valor não custa nada além dos mesmos bytes.
3. Substituir condicional por polimorfismo: quando isso melhora e quando só espalha a lógica?
Conceito: melhora quando o mesmo switch se repete em vários lugares. Piora quando há
um switch só — você espalha em N arquivos o que era legível numa tela.
Em Go: a alternativa intermediária, e frequentemente a melhor, é mapa de funções. Extensível como polimorfismo, e continua tudo num lugar. Reserve interface + tipos para quando cada caso tem estado e comportamento substanciais.
4. Por que refatoração nomeada permite commit pequeno e revisão fácil?
Conceito: porque cada uma é um movimento completo e verificável: compila, passa, tem um motivo, e reverte sozinha.
Em Go: somando gofmt (formatação nunca aparece no diff) com refatoração nomeada, o
diff mostra só a mudança estrutural. É a razão pela qual code review em Go tende a ser
mais rápido — não há ruído de formatação para separar do conteúdo.
Trade-offs
Do conceito:
- Refatorar compra custo de mudança menor; cobra tempo e risco de alterar comportamento sem perceber — risco que só teste elimina.
- Commits pequenos compram revisão e
bisect; cobram mais commits para ler no histórico. - Catálogo de nomes dá vocabulário e cria o risco de aplicar o movimento sem julgar se ele cabe.
Em Go:
gofmtnão configurável remove a discussão e remove a escolha.- Mapa de funções compra extensibilidade sem dispersão; cobra perder a verificação de
exaustividade (que Go também não dá no
switch, então o empate é técnico). - Extrair interface é a refatoração mais barata da linguagem: nenhuma linha do código existente muda.
Exemplo prático
Um método longo, com os três movimentos aplicados em sequência:
package main
import (
"errors"
"fmt"
"strings"
)
type Centavos int64
type Item struct {
Nome string
Preco Centavos
Qtd int
}
// ==================== ANTES: método longo, 1 nível de abstração misturado ====================
func reciboRuim(cliente, ruaNum, cidadeCep string, itens []Item, tipoDesc string) (string, error) {
// valida
if cliente == "" {
return "", errors.New("cliente vazio")
}
if len(itens) == 0 {
return "", errors.New("sem itens")
}
// soma
var sub Centavos
for _, i := range itens {
if i.Qtd <= 0 {
return "", fmt.Errorf("item %q com quantidade %d", i.Nome, i.Qtd)
}
sub += i.Preco * Centavos(i.Qtd)
}
// desconto
var desc Centavos
switch tipoDesc {
case "nenhum":
desc = 0
case "volume":
if sub > 10000 {
desc = sub * 10 / 100
}
case "cupom":
desc = 500
default:
return "", fmt.Errorf("desconto %q desconhecido", tipoDesc)
}
// formata
var b strings.Builder
b.WriteString("recibo de " + cliente + "\n")
b.WriteString(" " + ruaNum + ", " + cidadeCep + "\n")
for _, i := range itens {
fmt.Fprintf(&b, " %-10s %dx %d\n", i.Nome, i.Qtd, i.Preco)
}
fmt.Fprintf(&b, " subtotal %d desconto %d TOTAL %d\n", sub, desc, sub-desc)
return b.String(), nil
}
// ==================== 1. EXTRAIR CLASSE ====================
// Os campos ruaNum/cidadeCep sempre viajam juntos: são um conceito.
type Endereco struct {
Rua, Cidade string
}
func (e Endereco) String() string { return e.Rua + ", " + e.Cidade }
type Cliente struct {
Nome string
Endereco Endereco
}
// ==================== 2. SUBSTITUIR CONDICIONAL POR MAPA DE FUNÇÕES ====================
// Não por polimorfismo: cada caso é uma linha, e criar 3 tipos espalharia
// em 3 lugares algo que cabe numa tela. Mapa dá extensibilidade sem dispersão.
type regraDesconto func(subtotal Centavos) Centavos
var descontos = map[string]regraDesconto{
"nenhum": func(Centavos) Centavos { return 0 },
"volume": func(s Centavos) Centavos {
if s > 10000 {
return s * 10 / 100
}
return 0
},
"cupom": func(Centavos) Centavos { return 500 },
}
// ==================== 3. EXTRAIR MÉTODO ====================
// Cada comentário-seção do original virou uma função NOMEADA pelo conceito,
// não pela sequência de ações.
type Recibo struct {
Cliente Cliente
Itens []Item
Regra string
}
func (r Recibo) valida() error {
if r.Cliente.Nome == "" {
return errors.New("cliente vazio")
}
if len(r.Itens) == 0 {
return errors.New("sem itens")
}
for _, i := range r.Itens {
if i.Qtd <= 0 {
return fmt.Errorf("item %q com quantidade %d", i.Nome, i.Qtd)
}
}
return nil
}
func (r Recibo) subtotal() Centavos {
var s Centavos
for _, i := range r.Itens {
s += i.Preco * Centavos(i.Qtd)
}
return s
}
func (r Recibo) desconto(sub Centavos) (Centavos, error) {
regra, ok := descontos[r.Regra]
if !ok {
return 0, fmt.Errorf("desconto %q desconhecido", r.Regra)
}
return regra(sub), nil
}
func (r Recibo) formata(sub, desc Centavos) string {
var b strings.Builder
fmt.Fprintf(&b, "recibo de %s\n %s\n", r.Cliente.Nome, r.Cliente.Endereco)
for _, i := range r.Itens {
fmt.Fprintf(&b, " %-10s %dx %d\n", i.Nome, i.Qtd, i.Preco)
}
fmt.Fprintf(&b, " subtotal %d desconto %d TOTAL %d\n", sub, desc, sub-desc)
return b.String()
}
// O chamador agora lê como uma LISTA DE PASSOS, num nível só de abstração.
func (r Recibo) Gera() (string, error) {
if err := r.valida(); err != nil {
return "", fmt.Errorf("recibo: %w", err)
}
sub := r.subtotal()
desc, err := r.desconto(sub)
if err != nil {
return "", fmt.Errorf("recibo: %w", err)
}
return r.formata(sub, desc), nil
}
func main() {
itens := []Item{{"caneta", 500, 3}, {"caderno", 4000, 3}}
antes, _ := reciboRuim("ana", "rua A 1", "SP 01000", itens, "volume")
fmt.Print("ANTES:\n", antes)
r := Recibo{
Cliente: Cliente{"ana", Endereco{"rua A 1", "SP 01000"}},
Itens: itens,
Regra: "volume",
}
depois, _ := r.Gera()
fmt.Print("\nDEPOIS:\n", depois)
// mesmo comportamento: é isto que define "refatoração"
fmt.Println("\ncomportamento idêntico?", antes == depois)
// cada parte agora é testável sozinha
fmt.Println("\nsubtotal isolado:", r.subtotal())
d, _ := r.desconto(13500)
fmt.Println("desconto isolado:", d)
_, err := Recibo{Cliente: Cliente{Nome: "x"}, Itens: itens, Regra: "inexistente"}.Gera()
fmt.Println("regra inválida: ", err)
// adicionar uma regra nova: UMA linha, num lugar
descontos["black-friday"] = func(s Centavos) Centavos { return s / 2 }
r.Regra = "black-friday"
bf, _ := r.Gera()
fmt.Print("\nregra nova sem tocar em nada:\n", bf)
}
A linha que importa é comportamento idêntico? true. É a diferença entre refatoração e
reescrita — e o único jeito honesto de afirmar isso em código de verdade é ter testes
antes de começar, que é a semana 7.
Relacionado
- 1. Code Smells — o que cada refatoração corrige
- 2. Single Responsibility Principle — extrair classe é SRP aplicado
- 1. Teste Unitário — semana 7, o pré-requisito para poder afirmar "sem mudar comportamento"
- 5. Bisect — semana 8, commits pequenos são o que fazem o bisect funcionar
Parte de Semana 06 - Código Limpo e Refatoração · 00 - MOC Fundamentos de Programação