Fundamentos de Programação/02 - Código que Presta/Semana 05 - POO de Verdade7 min
Abstração
- Qual a diferença entre abstração e indireção? (uma reduz complexidade, a outra move)
- Quando uma abstração está no nível errado — vazando ou escondendo demais?
- Por que "abstração prematura" custa mais que duplicação?
- Você consegue nomear a abstração sem usar o nome da implementação?
Conceito
Abstração é remover detalhe para revelar a essência. Indireção é apenas acrescentar um nível. As duas parecem iguais no diagrama e são opostas no efeito.
Depois de você introduzir isso, quem lê o código precisa saber menos?
- Sim → abstração.
io.Readerte libera de saber se é arquivo, socket ou string. - Não, precisa saber o mesmo mais um nome → indireção.
UserServiceImplatrás deUserServiceatrás deIUserServicenão removeu nada.
Camada que não reduz o que você precisa saber é custo puro.
Toda abstração vaza (Joel Spolsky). Não é falha de projeto, é propriedade do conceito:
- um ORM esconde SQL — até você precisar do plano de execução (5. Índices)
- TCP esconde perda de pacote — até a latência importar
- memória virtual esconde o disco — até o swap começar
floatesconde a representação — até0.1 + 0.2 != 0.3
A consequência prática: você precisa entender um nível abaixo da abstração que usa. Não para usá-la no dia normal, mas para o dia em que ela vazar — e ela vai.
Nível errado, nos dois sentidos:
- vazando — a interface expõe conceitos da implementação:
BuscaUsuarioDoCacheRedis(). O nome do mecanismo está no contrato, então trocar o mecanismo quebra o contrato. - escondendo demais — você não consegue fazer o que precisa e tem que furar a
camada. Toda API que exige um "escape hatch" (
getRawConnection()) escondeu demais.
Abstração prematura custa mais que duplicação, e o motivo é preciso: com um caso de uso você não sabe qual é o eixo de variação. Você escolhe um eixo, quase sempre o errado, e uma abstração errada é mais difícil de remover que duplicação — porque código duplicado é local e visível, e abstração errada está espalhada e parece intencional.
Daí a regra dos três: espere o terceiro caso. Com dois você vê uma diferença; com três você vê o padrão.
O teste do nome: se você não consegue nomear a abstração sem usar o nome da implementação, você não abstraiu — só embrulhou.
Em Go
A cultura de Go é explicitamente hostil a abstração especulativa, e isso está nos provérbios da própria comunidade:
"The bigger the interface, the weaker the abstraction." "A little copying is better than a little dependency." — Rob Pike, Go Proverbs
O estudo de caso é io.Reader, e vale olhar com aten, porque é possivelmente a
melhor abstração já escrita numa biblioteca padrão:
type Reader interface {
Read(p []byte) (n int, err error)
}
Um método. E ele unifica: arquivo, conexão de rede, string em memória, buffer,
descompressor gzip, decifrador, corpo de requisição HTTP, saída de um comando, gerador
aleatório. Qualquer código que aceite io.Reader funciona com todos eles, e com os que
ainda não existem.
Note o que ela remove: você não sabe se há disco, rede, memória, latência, buffer ou
criptografia. O que ela preserva: leitura sequencial, contagem de bytes, e o fim
(io.EOF). É exatamente a essência de "fonte de bytes", e nada mais.
E note o que ela não tenta esconder: não há Seek, não há Close, não há tamanho.
Quem precisa disso pede outra interface (io.Seeker, io.Closer) — e a composição
(io.ReadCloser) monta o que faltar. Interfaces pequenas compõem; interfaces grandes
excluem.
"Aceite interfaces, devolva structs." Aceitar interface deixa o chamador escolher a implementação; devolver struct concreto não obriga o chamador a adivinhar o que aquela interface esconde e não amarra você a um contrato mais amplo do que precisa.
Go não tem herança, então abstração em Go é sempre interface + composição. Não existe a tentação de criar uma hierarquia abstrata de três níveis — a linguagem não oferece o mecanismo.
Respostas às perguntas-guia
1. Qual a diferença entre abstração e indireção?
Conceito: abstração reduz o que você precisa saber; indireção só move de lugar. O teste é se o leitor sabe menos depois.
Em Go: o exemplo canônico de indireção disfarçada é a interface com um implementador só criada "para desacoplar". Se ninguém mais vai implementar e você não usa para teste, ela é um nome extra no caminho.
2. Quando uma abstração está no nível errado — vazando ou escondendo demais?
Conceito: vazando quando o contrato menciona o mecanismo; escondendo demais quando você precisa de uma saída de emergência para fazer o trabalho.
Em Go: o sinal de "escondeu demais" é a função que devolve any ou aceita
map[string]any — ela virou genérica ao ponto de não abstrair nada. O sinal de "vazou" é
o nome do driver na assinatura.
3. Por que "abstração prematura" custa mais que duplicação?
Conceito: com um caso você não conhece o eixo de variação, escolhe errado, e abstração errada é mais difícil de remover que duplicação — duplicação é local e visível.
Em Go: é literalmente um provérbio da linguagem — a little copying is better than a little dependency. E há uma razão técnica extra: em Go, duplicar 20 linhas custa 20 linhas; criar um pacote para compartilhá-las custa uma dependência no grafo de importação, que é acíclico e imposto pelo compilador (4. Grafos).
4. Você consegue nomear a abstração sem usar o nome da implementação?
Conceito: se não consegue, não abstraiu.
Em Go, a convenção de nomes ajuda a testar isso: interface de um método se chama
<Verbo>er — Reader, Writer, Stringer, Formatter, Closer. Se o nome natural do
seu tipo é RedisUserRepository, o Redis no nome é a implementação avisando que ela
não foi abstraída.
Trade-offs
Do conceito:
- Abstração compra liberdade de trocar a implementação; cobra um nível de indireção e a chance de escolher o eixo errado.
- Abstração pequena compõe e cobre menos; abstração grande cobre mais e é mais difícil de satisfazer.
- Esperar o terceiro caso compra informação e cobra duplicação temporária — que é visível e barata de remover.
Em Go:
- Interfaces pequenas + satisfação implícita tornam a abstração quase gratuita depois que você conhece o eixo — outra razão para não antecipar.
- Devolver struct concreto compra clareza e cobra que o chamador dependa do seu tipo (o que é bom se o tipo é estável).
- Não ter herança remove uma ferramenta e uma classe inteira de erros (4. Composição vs Herança).
Exemplo prático
io.Reader em ação: a mesma função consumindo cinco fontes diferentes, sem saber de
nenhuma delas.
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"
)
// Não sabe se é arquivo, rede, memória, gzip. Sabe apenas "fonte de bytes".
func contaLinhas(r io.Reader) (int, error) {
buf := make([]byte, 4096)
n := 0
for {
lidos, err := r.Read(buf)
n += bytes.Count(buf[:lidos], []byte("\n"))
if err == io.EOF {
return n, nil
}
if err != nil {
return n, err
}
}
}
// Um io.Reader nosso: gera N linhas sem alocar nada.
type geradorDeLinhas struct{ faltam int }
func (g *geradorDeLinhas) Read(p []byte) (int, error) {
if g.faltam == 0 {
return 0, io.EOF
}
n := 0
for n < len(p) && g.faltam > 0 {
p[n] = '\n'
n++
g.faltam--
}
return n, nil
}
func gzipDe(texto string) io.Reader {
var b bytes.Buffer
w := gzip.NewWriter(&b)
w.Write([]byte(texto))
w.Close()
r, _ := gzip.NewReader(&b)
return r
}
func main() {
texto := "a\nb\nc\n"
fontes := []struct {
nome string
r io.Reader
}{
{"string em memória", strings.NewReader(texto)},
{"buffer de bytes", bytes.NewBufferString(texto)},
{"gzip descomprimindo", gzipDe(texto)},
{"nosso gerador", &geradorDeLinhas{faltam: 3}},
{"dois readers concatenados", io.MultiReader(
strings.NewReader("x\n"), strings.NewReader("y\n"))},
}
for _, f := range fontes {
n, err := contaLinhas(f.r)
fmt.Printf("%-26s -> %d linhas (err=%v)\n", f.nome, n, err)
}
// composição: interfaces pequenas se somam
var rc io.ReadCloser = io.NopCloser(strings.NewReader(texto))
n, _ := contaLinhas(rc) // ReadCloser satisfaz Reader
rc.Close()
fmt.Printf("%-26s -> %d linhas\n", "ReadCloser (composto)", n)
}
contaLinhas tem 12 linhas, nunca vai mudar, e funciona com fontes que não existiam
quando ela foi escrita. Isso é o que abstração entrega quando está no nível certo.
Relacionado
- 3. Interfaces — o mecanismo que expressa abstração em Go
- 1. Encapsulamento — esconder a decisão é o pré-requisito
- 4. Composição vs Herança — como Go monta abstração sem hierarquia
- 1. Code Smells — semana 6, abstração errada aparece como smell
Parte de Semana 05 - POO de Verdade · 00 - MOC Fundamentos de Programação