Fundamentos de Programação/02 - Código que Presta/Semana 05 - POO de Verdade8 min
Interfaces
- Para que serve uma interface se só existe uma implementação?
- Quem deveria definir a interface: quem implementa ou quem consome?
- Como interfaces pequenas viabilizam teste sem mock de tudo? (ver 4. Mocks e Stubs)
- Qual a relação disso com 3. Dependency Inversion Principle?
Conceito
Uma interface é um contrato: um conjunto de operações, sem implementação. Ela separa o que pode ser feito de como é feito.
Duas escolas, e a diferença é grande:
| Nominal / explícita | Estrutural / implícita | |
|---|---|---|
| Como satisfaz | o tipo declara (implements) |
por ter os métodos |
| Exemplos | Java, C# | Go, TypeScript |
| Vantagem | intenção explícita, fácil de descobrir | satisfaz contratos que você não conhecia |
| Custo | o implementador precisa conhecer a interface | satisfação acidental é possível |
A consequência da estrutural é o que importa: você pode escrever uma interface para um tipo de terceiro, sem tocar no código dele. Isso muda quem manda no contrato.
Se o produtor define, ele publica um contrato amplo ("tudo que meu tipo faz") e todos os consumidores passam a depender do pacote dele — inclusive de partes que não usam. Você acoplou tudo ao produtor, que é o oposto do objetivo.
Se o consumidor define, ele declara exatamente o mínimo que precisa. O produtor não sabe que a interface existe, e a dependência aponta para dentro do consumidor.
Essa é a inversão de dependência de verdade (3. Dependency Inversion Principle) — e é por isso que ela é natural em Go e cerimoniosa em Java.
Para que serve uma interface com uma implementação só? Três respostas honestas:
- para testar — substituir o real por um dublê (4. Mocks e Stubs)
- para inverter a dependência — o domínio define o contrato, a infra implementa
- para nada — e esse caso é o mais comum. Se não é (1) nem (2), é indireção (2. Abstração)
Interface pequena é melhor por três razões acumulativas: mais fácil de satisfazer (mais tipos servem), mais fácil de dublar em teste (menos métodos falsos), e mais fácil de compor (você soma duas em vez de escolher uma grande).
Em Go
Satisfação implícita. Nenhum implements. O tipo satisfaz por ter os métodos — e
frequentemente satisfaz interfaces que o autor nunca viu.
Uma interface vale 16 bytes: um par (ponteiro para informação de tipo, ponteiro para
o dado). Converter um valor concreto em interface é boxing, e frequentemente força
alocação no heap (2. Stack vs Heap). É o custo escondido de any em caminho quente.
A interface só é nil quando os dois ponteiros são nil. Um *MeuErro nil dentro
de um error produz uma interface não-nil:
func falha() error {
var e *MeuErro // nil
return e // interface = (tipo=*MeuErro, dado=nil) -> NÃO é nil
}
if err := falha(); err != nil { /* entra aqui */ }
Regra: nunca declare um ponteiro de erro concreto para devolvê-lo. Devolva nil
literal. Detalhado em 1. Tipos e Sistema de Tipos.
As interfaces canônicas vale conhecer de cor, porque metade da stdlib fala nelas:
| Interface | Métodos | Para quê |
|---|---|---|
error |
Error() string |
o único mecanismo de erro da linguagem |
io.Reader / io.Writer |
1 cada | fonte e destino de bytes |
fmt.Stringer |
String() string |
como o valor se imprime |
sort.Interface |
3 | ordenação customizada (pré-generics) |
context.Context |
4 | cancelamento e prazo |
Composição de interfaces: io.ReadWriter é literalmente
interface { Reader; Writer }. Interface embutida em interface é a forma padrão de somar
contratos.
Interface embutida em struct permite implementação parcial: você embute a interface,
implementa só os métodos que interessam, e os outros existem (com receiver nil, que dá
panic se chamado). É comum em dublês de teste e em wrappers de http.ResponseWriter.
Verificação em tempo de compilação — o idioma que garante que você não quebrou o contrato sem perceber:
var _ io.Writer = (*MeuTipo)(nil) // não compila se MeuTipo não satisfizer
Receiver de valor vs ponteiro decide o método set: se você declara
func (m *Meu) Write(...), então *Meu satisfaz io.Writer, mas Meu não. É a
fonte mais comum de "cannot use x (type Meu) as io.Writer".
Respostas às perguntas-guia
1. Para que serve uma interface se só existe uma implementação?
Conceito: para teste, para inverter dependência, ou para nada. O terceiro caso é o mais frequente.
Em Go: como a satisfação é implícita, você pode esperar — criar a interface no dia
em que precisar, sem tocar no tipo concreto. Em Java você teria que voltar e adicionar
implements. Isso é o argumento definitivo contra criar interface "por precaução" em Go:
o custo de adicionar depois é zero.
2. Quem deveria definir a interface: quem implementa ou quem consome?
Conceito: quem consome. Assim o contrato é o mínimo necessário e a dependência aponta para dentro.
Em Go: é o idioma padrão, e a stdlib faz exatamente isso — io.Reader foi definida por
quem lê, não por quem produz bytes. Nenhum arquivo, socket ou buffer sabe que
io.Reader existe.
3. Como interfaces pequenas viabilizam teste sem mock de tudo?
Conceito: porque o dublê precisa implementar poucos métodos. Uma interface de 12 métodos obriga o teste a falsificar 12 comportamentos, e 11 deles são ruído.
Em Go: se o seu caso de uso só lê um usuário, declare
type buscadorDeUsuario interface { Busca(id int) (Usuario, error) } — um método. O
dublê tem três linhas e nenhuma biblioteca de mock é necessária (4. Mocks e Stubs).
4. Qual a relação disso com 3. Dependency Inversion Principle?
Conceito: interface definida pelo consumidor é inversão de dependência: a política declara o contrato, o detalhe se adapta.
Em Go: a satisfação implícita torna isso literal — o pacote de domínio declara a interface e não importa o pacote de infra. A flecha de dependência aponta de infra para domínio, e o compilador garante que não há ciclo.
Trade-offs
Do conceito:
- Estrutural compra flexibilidade (satisfaz sem saber) e cobra descoberta (quem implementa o quê não está escrito em lugar nenhum) e satisfação acidental.
- Nominal compra intenção explícita e cobra que o implementador conheça o contrato.
- Interface pequena compõe e exige mais interfaces; grande cobre mais e exclui mais implementadores.
Em Go:
- Satisfação implícita + interface no consumidor é a combinação que torna inversão de
dependência natural. O custo: para saber quem implementa
X, você precisa de ferramenta (go doc, IDE), não de leitura. - Interface custa boxing e possível alocação — em código quente, tipo concreto ou generics evitam isso.
- Receiver ponteiro vs valor no método set é uma fonte permanente de erro de compilação para quem está aprendendo.
Exemplo prático
package main
import (
"errors"
"fmt"
"io"
"strings"
)
// ---------- Interface definida pelo CONSUMIDOR, com o mínimo necessário ----------
type Usuario struct {
ID int
Nome string
}
// O caso de uso declara o que precisa: UM método.
type buscadorDeUsuario interface {
Busca(id int) (Usuario, error)
}
type SaudacaoService struct{ repo buscadorDeUsuario }
func (s SaudacaoService) Saudacao(id int) (string, error) {
u, err := s.repo.Busca(id)
if err != nil {
return "", fmt.Errorf("saudação para %d: %w", id, err)
}
return "olá, " + u.Nome, nil
}
// Implementação "real": tem MUITO mais métodos, e o serviço não sabe de nenhum
type RepoCompleto struct{ dados map[int]Usuario }
func (r RepoCompleto) Busca(id int) (Usuario, error) {
if u, ok := r.dados[id]; ok {
return u, nil
}
return Usuario{}, errors.New("não encontrado")
}
func (r RepoCompleto) Salva(u Usuario) error { r.dados[u.ID] = u; return nil }
func (r RepoCompleto) Remove(id int) error { delete(r.dados, id); return nil }
func (r RepoCompleto) Conta() int { return len(r.dados) }
// Dublê de teste: três linhas, sem biblioteca de mock
type repoFalso struct{ u Usuario; err error }
func (r repoFalso) Busca(int) (Usuario, error) { return r.u, r.err }
// ---------- Composição de interfaces ----------
type Contador interface{ Conta() int }
// Verificação em tempo de compilação
var (
_ buscadorDeUsuario = RepoCompleto{}
_ buscadorDeUsuario = repoFalso{}
_ Contador = RepoCompleto{}
)
// ---------- A armadilha do nil ----------
type MeuErro struct{}
func (e *MeuErro) Error() string { return "meu erro" }
func devolveErradamente() error {
var e *MeuErro // nil
return e // interface NÃO-nil com dado nil
}
func devolveCorretamente() error { return nil }
// ---------- Receiver ponteiro decide o method set ----------
type escritorValor struct{ n int }
type escritorPonteiro struct{ n int }
func (e escritorValor) Write(p []byte) (int, error) { return len(p), nil }
func (e *escritorPonteiro) Write(p []byte) (int, error) { e.n += len(p); return len(p), nil }
func main() {
real := RepoCompleto{dados: map[int]Usuario{1: {1, "ana"}}}
fmt.Println(SaudacaoService{real}.Saudacao(1))
fmt.Println(SaudacaoService{real}.Saudacao(99))
// mesmo serviço, dublê de 3 linhas
fmt.Println(SaudacaoService{repoFalso{u: Usuario{2, "dublê"}}}.Saudacao(2))
_, err := SaudacaoService{repoFalso{err: errors.New("banco caiu")}}.Saudacao(3)
fmt.Println("erro do dublê:", err)
// a armadilha
fmt.Println("errado == nil?", devolveErradamente() == nil) // false!
fmt.Println("correto == nil?", devolveCorretamente() == nil) // true
// method set: valor satisfaz, ponteiro também; só-ponteiro NÃO deixa o valor satisfazer
var w io.Writer = escritorValor{}
fmt.Fprint(w, "ok")
var w2 io.Writer = &escritorPonteiro{} // &, obrigatório
fmt.Fprint(w2, "ok")
// var w3 io.Writer = escritorPonteiro{} // NÃO compila
fmt.Println("method set: ok")
// io.Reader: a abstração de um método
var r io.Reader = strings.NewReader("bytes")
b, _ := io.ReadAll(r)
fmt.Printf("io.Reader deu %q\n", b)
}
Relacionado
- 2. Abstração — interface é o mecanismo; abstração é a decisão
- 1. Encapsulamento — a interface é o que sobra depois de esconder o resto
- 3. Dependency Inversion Principle — semana 6, interface no consumidor é a inversão
- 4. Mocks e Stubs — semana 7, interface pequena é o que torna o dublê barato
- 1. Tipos e Sistema de Tipos — semana 1, o par (tipo, dado) e a armadilha do nil
Parte de Semana 05 - POO de Verdade · 00 - MOC Fundamentos de Programação