trilha

Fundamentos de Programação/02 - Código que Presta/Semana 05 - POO de Verdade8 min

Interfaces

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

Quem deve definir a interface: o consumidor

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:

  1. para testar — substituir o real por um dublê (4. Mocks e Stubs)
  2. para inverter a dependência — o domínio define o contrato, a infra implementa
  3. 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 armadilha nº 1: interface não-nil com ponteiro nil

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


Parte de Semana 05 - POO de Verdade · 00 - MOC Fundamentos de Programação

Buscar

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