trilha

Fundamentos de Programação/02 - Código que Presta/Semana 06 - Código Limpo e Refatoração10 min

Refatorações Nomeadas

Perguntas-guia
  • 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:

  1. vocabulário — "extraia esse método" comunica mais rápido que descrever o movimento
  2. passos mecânicos — cada refatoração tem uma receita verificável, que reduz a chance de mudar comportamento por acidente
  3. 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, cepEndereco), 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:

  • gofmt nã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


Parte de Semana 06 - Código Limpo e Refatoração · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas