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 , 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