Aula 08 · 45–65 minutos

Um segundo use case: consultar pedido

Aula 8 — o sistema só sabia escrever, nunca ler de volta; por que engordar PedidoRepository com Buscar viola o Interface Segregation Principle, e como PedidoFinder, uma interface pequena e separada, resolve isso.

Resultado e mapa da aula

Desde a Lição 4, o aprovador-pedidos sabe fazer exatamente uma coisa com um pedido: decidir e salvar (AprovarPedido, RejeitarPedido — Lição 6). Mas ninguém consegue perguntar ao sistema "o que aconteceu com o pedido 42?" — não existe nenhum caminho de leitura. Esta aula adiciona o primeiro caso de uso de consulta, e usa esse problema simples para ensinar um princípio SOLID que as lições anteriores já aplicavam na prática, sem nomear: o Interface Segregation Principle.

  1. O problema: um sistema que só escreve nunca responde a uma pergunta.
  2. Uma abordagem plausível, mas incorreta: adicionar Buscar direto em PedidoRepository.
  3. Fonte primária: o Interface Segregation Principle, de Robert C. Martin — e sua tradução idiomática para Go.
  4. A correção: PedidoFinder, uma segunda interface, pequena e separada.
  5. Um único tipo concreto satisfazendo duas interfaces ao mesmo tempo — a satisfação implícita da Lição 2, de um ângulo novo.
  6. O padrão de erro "não encontrado" — um valor sentinela, não uma string mágica.

O problema: um sistema que só escreve

Repare no que PedidoRepository sabe fazer, desde a Lição 6:

// internal/usecase/aprovar_pedido.go — como está hoje
type PedidoRepository interface {
    Salvar(p domain.Pedido) error
}

Um único método, Salvar. Isso é suficiente para AprovarPedido e RejeitarPedido, porque os dois só produzem uma decisão e a gravam. Mas nenhum caso de uso do sistema consegue responder à pergunta mais básica que alguém faria sobre um pedido: qual é o status dele agora? Um atendente de suporte, ou o próprio dono do pedido, não tem como saber se a decisão já foi tomada — o sistema grava, mas nunca lê de volta.

Uma abordagem plausível, mas incorreta: engordar PedidoRepository

Por que essa abordagem parece razoável: já existe uma interface chamada PedidoRepository — "repositório de pedidos" soa como o lugar certo para qualquer operação relacionada a pedidos, escrita ou leitura. Adicionar Buscar ali evita criar uma interface nova.

Por que falha: AprovarPedido e RejeitarPedido nunca chamam Buscar — os dois só chamam Salvar. Depois dessa mudança, os dois use cases passam a depender de uma interface que exige um método que eles nunca usam. Isso tem consequência prática imediata: qualquer fake de teste escrito para AprovarPedido — como repositorioFake, da Lição 4 — precisaria implementar Buscar só para satisfazer a interface, mesmo que nenhum teste de AprovarPedido jamais chame esse método. É atrito sem benefício: código a mais para manter, sem nenhuma necessidade real por trás dele.

Fonte primária: o Interface Segregation Principle

Esse mesmo princípio já apareceu nesta trilha antes, sem o nome formal: a Lição 4 perguntou, nas questões de entrevista, "o que aconteceria se você adicionasse um método a PedidoRepository que só a implementação memoria usa, mas usecase nunca chama?" — a resposta certa, na época, já descrevia o ISP sem citá-lo pelo nome. Esta aula fecha esse ciclo, formalizando o princípio e aplicando-o de propósito, não por acidente.

A correção: PedidoFinder, uma interface pequena e separada

Em vez de engordar PedidoRepository, o caso de uso de consulta declara sua própria interface — seguindo a mesma convenção da Lição 2: quem usa a interface é quem a declara, e ConsultarPedido só precisa de um método.

// internal/usecase/consultar_pedido.go
package usecase

import "github.com/allangrds/aprovador-pedidos/internal/domain"

type PedidoFinder interface {
    Buscar(id string) (domain.Pedido, error)
}

type ConsultarPedido struct {
    finder PedidoFinder
}

func NovoConsultarPedido(f PedidoFinder) *ConsultarPedido {
    return &ConsultarPedido{finder: f}
}

func (uc *ConsultarPedido) Executar(id string) (domain.Pedido, error) {
    return uc.finder.Buscar(id)
}

PedidoRepository continua exatamente como estava — um método, Salvar, usado só por quem escreve. PedidoFinder é uma interface nova, com um método, Buscar, usada só por quem lê. Nenhuma das duas cresceu para acomodar a outra.

Um tipo concreto, duas interfaces — sem escolher entre elas

A implementação em memória ganha o método que faltava, e passa a satisfazer as duas interfaces ao mesmo tempo — sem precisar declarar isso em lugar nenhum, a mesma satisfação implícita da Lição 2:

// internal/memoria/pedido_repository.go
package memoria

import (
    "errors"

    "github.com/allangrds/aprovador-pedidos/internal/domain"
)

var ErrPedidoNaoEncontrado = errors.New("pedido não encontrado")

type PedidoRepository struct {
    pedidos map[string]domain.Pedido
}

func NovoPedidoRepository() *PedidoRepository {
    return &PedidoRepository{pedidos: map[string]domain.Pedido{}}
}

func (r *PedidoRepository) Salvar(p domain.Pedido) error {
    r.pedidos[p.ID] = p
    return nil
}

func (r *PedidoRepository) Buscar(id string) (domain.Pedido, error) {
    p, ok := r.pedidos[id]
    if !ok {
        return domain.Pedido{}, ErrPedidoNaoEncontrado
    }
    return p, nil
}

memoria.PedidoRepository nunca declara implements PedidoRepository, PedidoFinder — Go não tem essa sintaxe. Ele simplesmente tem os dois métodos com as assinaturas certas, e satisfaz as duas interfaces ao mesmo tempo, cada uma usada por um consumidor diferente:

Interfaces segregadas: escrita e leitura separadasAprovarPedido e RejeitarPedido dependem só de PedidoRepository, que declara Salvar. ConsultarPedido depende só de PedidoFinder, que declara Buscar. Nenhum dos três casos de uso depende de um método que não usa. memoria.PedidoRepository, um único tipo concreto, satisfaz as duas interfaces ao mesmo tempo, por satisfação implícita.AprovarPedidoRejeitarPedidoPedidoRepositorySalvar(p) — só issodepende deConsultarPedidoPedidoFinderBuscar(id) — só issodepende dememoria.PedidoRepositoryum tipo concreto — satisfaz as duas interfacessatisfaz — sem importsatisfaz — sem importNenhum dos três use cases depende deum método que nunca chama.

AprovarPedido e RejeitarPedido dependem só de PedidoRepository (Salvar). ConsultarPedido depende só de PedidoFinder (Buscar). Nenhum dos três depende de um método que não usa. memoria.PedidoRepository, um único tipo concreto, satisfaz as duas interfaces ao mesmo tempo — a mesma satisfação implícita da Lição 2, aplicada a duas interfaces pequenas em vez de uma só.

main.go monta as três peças a partir do mesmo repositório:

// main.go
repo := memoria.NovoPedidoRepository()
aprovarPedido := usecase.NovoAprovarPedido(repo)
rejeitarPedido := usecase.NovoRejeitarPedido(repo)
consultarPedido := usecase.NovoConsultarPedido(repo)

pedido := domain.Pedido{ID: "1", Total: 750}
if _, err := aprovarPedido.Executar(pedido); err != nil {
    fmt.Println("erro:", err)
    return
}

encontrado, err := consultarPedido.Executar("1")
if err != nil {
    fmt.Println("erro:", err)
    return
}
fmt.Printf("pedido %s está com status %s\n", encontrado.ID, encontrado.Status)

O mesmo valor repo é passado três vezes, para três parâmetros de tipos diferentes (PedidoRepository, PedidoRepository de novo, PedidoFinder) — o compilador aceita porque *memoria.PedidoRepository satisfaz as três chamadas, cada uma exigindo só o subconjunto de métodos que aquele parâmetro específico declara.

Prática guiada: um terceiro caso de leitura

Raciocínio antes do código

ConsultarPedido usa Buscar(id string); o novo caso de uso precisa de algo como Listar() ([]domain.Pedido, error) — uma operação de leitura diferente, com uma assinatura diferente. A pergunta central do exercício é exatamente a mesma que motivou a aula inteira, aplicada de novo: dois consumidores diferentes, dois métodos diferentes — eles deveriam compartilhar uma interface, ou não?

Uma abordagem plausível, mas incorreta: agrupar por serem os dois "leitura"

// internal/usecase/consultar_pedido.go — abordagem incorreta
type PedidoFinder interface {
    Buscar(id string) (domain.Pedido, error)
    Listar() ([]domain.Pedido, error)
}

Por que falha: é o mesmo erro do início da aula, só que motivado por uma categoria ("leitura") em vez de um nome ("repositório"). ConsultarPedido nunca chama Listar — agrupar os dois métodos numa interface só porque os dois são operações de leitura reintroduz exatamente a dependência espúria que a correção principal desta aula eliminou. Um fake de teste para ConsultarPedido.Executar precisaria, de novo, implementar um método que o teste nunca chama.

A solução correta: uma terceira interface, do mesmo tamanho que as outras duas

// internal/usecase/listar_pedidos_pendentes.go
package usecase

import "github.com/allangrds/aprovador-pedidos/internal/domain"

type PedidoLister interface {
    Listar() ([]domain.Pedido, error)
}

type ListarPedidosPendentes struct {
    lister PedidoLister
}

func NovoListarPedidosPendentes(l PedidoLister) *ListarPedidosPendentes {
    return &ListarPedidosPendentes{lister: l}
}

func (uc *ListarPedidosPendentes) Executar() ([]domain.Pedido, error) {
    todos, err := uc.lister.Listar()
    if err != nil {
        return nil, err
    }
    var pendentes []domain.Pedido
    for _, p := range todos {
        if p.Status == domain.StatusPendente {
            pendentes = append(pendentes, p)
        }
    }
    return pendentes, nil
}

Por que está correto: a mesma pergunta — "quem usa este método?" — recebe a mesma resposta consistente de toda a aula. Buscar e Listar continuam em interfaces separadas, não porque exista uma regra "uma interface, um método sempre", mas porque, neste projeto, nenhum caso de uso hoje precisa dos dois ao mesmo tempo. memoria.PedidoRepository ganha um terceiro método, Listar, e passa a satisfazer três interfaces pequenas em vez de duas — o mesmo tipo concreto, crescendo em capacidade sem forçar nenhum consumidor existente a depender de mais do que já dependia.

Verificação: escreva um fake mínimo para ListarPedidosPendentes, implementando só Listar — confirme que esse fake não precisa de nenhum método Buscar ou Salvar para compilar, prova concreta de que a interface está do tamanho certo para esse consumidor específico.

Perguntas de entrevista e de mercado

"O que é o Interface Segregation Principle, na sua própria definição?" Uma boa resposta evita repetir só a frase decorada. Explique o raciocínio: quando uma interface cresce para acomodar métodos usados por consumidores diferentes, cada consumidor individual passa a depender de métodos que não usa — um acoplamento desnecessário, que se manifesta como atrito real em testes (fakes precisando implementar métodos não usados) e em manutenção (mudar um método usado só por um consumidor pode quebrar a compilação de outro que nunca o chamou).

"Neste projeto, por que não existe uma única interface PedidoRepository com todos os métodos de acesso a dados de pedido?" Cite a divisão concreta: PedidoRepository (Salvar, usado por quem decide) e PedidoFinder (Buscar, usado por quem consulta) são interfaces separadas porque nenhum caso de uso precisa das duas operações ao mesmo tempo. Reforce que isso não é dogma de "sempre separar tudo" — é consequência de quem realmente consome cada método.

"Como um único tipo, memoria.PedidoRepository, consegue satisfazer duas interfaces diferentes ao mesmo tempo, sem declarar isso explicitamente?" Volte à satisfação implícita da Lição 2: Go verifica a satisfação de interface estruturalmente, no ponto de uso, não por uma declaração como implements. Um tipo concreto só precisa ter os métodos com as assinaturas certas; quantas interfaces diferentes ele satisfaz ao mesmo tempo é irrelevante para o compilador — cada consumidor declara a interface do tamanho que precisa, e o mesmo tipo concreto pode atender vários consumidores diferentes.

"O conselho de mercado 'accept interfaces, return structs' — o que ele quer dizer, e como aparece neste projeto?" Explique as duas metades: funções e construtores deveriam aceitar parâmetros do tipo mais genérico possível (uma interface pequena, como PedidoFinder), para maximizar quantos tipos concretos podem ser passados; e deveriam retornar tipos concretos (como domain.Pedido, não uma interface), para que quem recebe o valor tenha acesso a todos os seus campos e métodos, sem precisar de uma asserção de tipo. Cite NovoConsultarPedido(f PedidoFinder) como o primeiro; Buscar retornando domain.Pedido (não uma interface) como o segundo.

Recupere da memória: 10 questões

Recupere da memória: 10 questões

Antes desta aula, o que faltava no aprovador-pedidos para responder à pergunta 'qual é o status do pedido 42'?

Segundo a citação de Robert C. Martin desta aula, o que o Interface Segregation Principle afirma?

Por que adicionar Buscar diretamente em PedidoRepository (a abordagem incorreta desta aula) é um problema para AprovarPedido e RejeitarPedido?

Qual interface ConsultarPedido depende, e quantos métodos ela declara?

Como memoria.PedidoRepository consegue satisfazer PedidoRepository e PedidoFinder ao mesmo tempo, sem nenhuma declaração explícita de 'implements'?

Por que ErrPedidoNaoEncontrado é declarado como uma variável de pacote com errors.New, em vez de criar uma nova string de erro a cada chamada de Buscar que falha?

No exemplo de main.go desta aula, a mesma variável repo é passada para NovoAprovarPedido, NovoRejeitarPedido e NovoConsultarPedido. Isso é possível porque...

Segundo o conselho de mercado citado nesta aula ('accept interfaces, return structs'), o que NovoConsultarPedido(f PedidoFinder) exemplifica?

No exercício desta aula, por que agrupar Buscar e Listar numa única interface PedidoFinder (só porque os dois são operações de leitura) é uma abordagem incorreta?

Depois do exercício desta aula, quantas interfaces pequenas de leitura/escrita o pacote usecase declara ao todo, e o que cada uma exige?

Feche o modelo mental

Um sistema que só escreve não responde a perguntas. A tentação de adicionar Buscar direto em PedidoRepository parece econômica, mas força AprovarPedido e RejeitarPedido a depender de um método que nunca chamam — exatamente o que o Interface Segregation Principle proíbe: "clients should not be forced to depend on methods they do not use". A correção é PedidoFinder, uma segunda interface pequena, declarada por quem a usa (ConsultarPedido), do mesmo jeito que PedidoRepository já era declarada por quem a usava desde a Lição 2. Um único tipo concreto — memoria.PedidoRepository — satisfaz as duas interfaces ao mesmo tempo, sem nenhuma declaração explícita, pela mesma satisfação implícita de sempre. O exercício estendeu o mesmo raciocínio a um terceiro método de leitura, confirmando que o critério certo nunca foi "métodos parecidos ficam juntos" — é "quem realmente usa este método".

Próxima aula planejada: DTOs — o formato que já existia, desde o exercício da Lição 4, agora com um nome formal.

Referências primárias para aprofundar

O princípio central desta aula é de Robert C. Martin, "The Interface Segregation Principle", C++ Report (junho de 1996), elaborado em Design Principles and Design Patterns (2000), já citado na Lição 1. A aplicação idiomática a Go vem de SOLID Go Design, Dave Cheney (2016) e dos Go Proverbs, Rob Pike, já citados na Lição 6.

Consulte também o glossário desta trilha.