Aula 01 · 40 minutos
Inverter a seta: DI não é DIP
Aula 1 — diferencie injeção de dependência de inversão de dependência e aplique DIP em Go.
Resultado, pré-requisito e mapa
Ao fim, você conseguirá revisar um caso de uso Go e responder, com evidência no código: houve apenas injeção de dependência (DI) ou também inversão de dependência (DIP)? Você também conseguirá extrair um contrato mínimo, montar a aplicação na borda e testar a regra sem banco.
Pré-requisito: ler structs, métodos, interfaces e construtores em Go. Esta aula não ensina SQL, HTTP ou um framework de DI; ela estabelece o modelo mental que as próximas aulas aplicam a contratos, composição, testes e refatoração.
- Veremos o acoplamento que existe mesmo com um construtor.
- Separaremos a direção estática do código da chamada que ocorre em execução.
- Montaremos a solução e testaremos sua consequência observável.
- Decidiremos quando não criar uma interface.
O problema aparece antes do teste
Imagine que checkout contém a política de criar um pedido. A aplicação já usa injeção por construtor: alguém cria o repositório e o entrega ao serviço. Ainda assim, a regra conhece PostgreSQL pelo nome.
// package checkout
type Service struct {
orders *postgres.OrderRepository
}
func NewService(orders *postgres.OrderRepository) *Service {
return &Service{orders: orders}
}
DI responde somente como o objeto recebe um colaborador: por construtor, por exemplo. Ela já evita que Service construa a conexão sozinho. Mas o campo, o parâmetro e o import ainda apontam de checkout para postgres. Se o detalhe mudar, a regra precisa mudar junto; se o teste quiser outra implementação, ele ainda precisa conhecer a infraestrutura.
DEPENDÊNCIA ESTÁTICA (compilação)
checkout ─────────────────────────────→ postgres
regra de negócio detalhe de infraestrutura
Consequência real: o problema não é que PostgreSQL seja ruim. É a política de negócio carregar uma decisão que pertence à borda da aplicação: qual tecnologia persiste o pedido.
DI e DIP respondem perguntas diferentes
DI pode realizar DIP, mas não o garante. Uma frase segura para revisão ou entrevista é: "Injetar um concreto separa criação e uso; depender de uma abstração definida pelo consumidor separa a política do detalhe."
A inversão: o consumidor declara sua necessidade
O serviço não precisa saber como um pedido é salvo; precisa apenas solicitar que ele seja salvo. Essa necessidade vira um contrato pequeno no pacote que a consome.
// package checkout
type OrderSaver interface {
Save(ctx context.Context, order Order) error
}
type Service struct { orders OrderSaver }
func NewService(orders OrderSaver) *Service {
return &Service{orders: orders}
}
Em Go, *postgres.OrderRepository satisfaz OrderSaver implicitamente se tiver o método Save com a mesma assinatura. O adaptador não importa checkout para declarar que implementa a interface; o compilador verifica a compatibilidade quando ele é passado ao construtor.
DEPENDÊNCIA ESTÁTICA (compilação)
checkout ──→ OrderSaver ←── postgres
política contrato detalhe
main importa checkout e postgres para conectá-los.
É essa seta do código-fonte que se inverte. A direção da chamada em execução continua natural: a regra chama Save; o valor concreto que recebe a chamada pode ser PostgreSQL em produção.
Quando o código executa — e por quê
- Compilação: Go verifica que o valor entregue a
NewServicesatisfazOrderSaver. Isso protege a forma do contrato, não a conectividade com o banco. - Inicialização:
main, o ponto de composição, abre recursos, cria o repositório e escolhe injetá-lo. Ele pode conhecer detalhes porque sua função é montar a aplicação. - Operação: o adaptador de entrada chama o caso de uso;
Servicevalida a regra e chamaSavepelo contrato.mainnão participa dessa chamada. - Teste unitário: o teste cria um fake e o injeta. A mesma regra executa sem processo de banco, rede, schema ou dados compartilhados.
Abra o diagrama e compare as visões "Composição em produção" e "Composição no teste". Em ambas, checkout depende de OrderSaver; o que muda é o valor escolhido na borda.
Implementação, composição e evidência de teste
O repositório existente não precisa ser reescrito se já oferece o método esperado; só a forma como a regra o enxerga muda.
// package main — composição: cria detalhes uma vez, na inicialização.
repo := postgres.NewOrderRepository(db)
service := checkout.NewService(repo)
// package checkout_test — teste da regra, sem PostgreSQL.
type fakeOrderSaver struct { saved checkout.Order }
func (f *fakeOrderSaver) Save(_ context.Context, order checkout.Order) error {
f.saved = order
return nil
}
func TestCreatePersistsTheOrder(t *testing.T) {
fake := &fakeOrderSaver{}
service := checkout.NewService(fake)
// Execute Create e compare fake.saved com o pedido esperado.
}
A asserção deve provar um efeito da regra — o pedido entregue ao contrato ou a reação a um erro controlado. SQL, migrations e conexão pertencem a testes de integração do adaptador PostgreSQL.
Alternativas que parecem boas, mas falham
Interface no pacote de infraestrutura
postgres.Repository com Save, Find, Delete e outros métodos descreve a capacidade do banco, não a necessidade de checkout. Ela transforma detalhes em API para todo consumidor e força contratos maiores que o uso real.
Interface criada antes de um consumidor
Uma interface para cada struct "por padrão" acrescenta indireção e nomes, sem remover um acoplamento demonstrável. Em Go, comece por tipos concretos e extraia o contrato quando surgir uma borda, variação ou teste com necessidade real.
Service locator ou banco criado dentro do serviço
Essas abordagens escondem a colaboração. O construtor deixa de declarar o que a regra precisa, a inicialização se espalha e o teste precisa de caminhos especiais. A composição explícita em main é mais fácil de ler, revisar e encerrar.
Segurança: uma abstração não autoriza uma operação
OrderSaver reduz acoplamento; ele não prova que alguém pode salvar um pedido nem que o pedido é válido. Em uma aplicação real, o adaptador de entrada valida a entrada e obtém a identidade de uma fonte confiável; a regra valida transições de negócio e escopo; o adaptador de persistência usa consultas parametrizadas e retorna erros tratados sem vazar detalhes internos.
Não coloque credenciais, SQL concatenado ou decisões de autorização dentro do exemplo de contrato. Esses detalhes devem ser implementados e verificados nas bordas corretas. DIP melhora a testabilidade desses controles, mas não os substitui.
Recupere da memória
Em Go, quem deve declarar a interface mínima para salvar um pedido quando somente o checkout precisa desse comportamento?
Feche o modelo mental
DI entrega uma colaboração. DIP faz a política depender de um contrato definido por sua necessidade, enquanto os detalhes se adaptam a ele. Use uma interface pequena quando ela isola uma fronteira real; mantenha um concreto quando não há variação, borda ou teste que justifique a abstração.
Próxima: Aula 02 — O contrato do consumidor.
Referências para aprofundar
A recomendação de declarar interfaces no consumidor e não antecipá-las apenas para mocking está no Go Code Review Comments — Interfaces. A implementação implícita está na especificação da linguagem Go.
Para a formulação do princípio, leia The Dependency Inversion Principle, de Robert C. Martin. Para DI como separação entre configuração e uso, consulte Inversion of Control Containers and the Dependency Injection pattern, de Martin Fowler.
Guia de solução do exercício
Fluxo de execução: O construtor recebe uma dependência na inicialização; durante a operação, a regra chama Save. A pergunta é quem possui o contrato dessa chamada.
Abordagem inadequada: Apenas injetar um tipo de infraestrutura continua acoplando checkout ao pacote postgres.
// package checkout
type Service struct { orders *postgres.OrderRepository }
Por que falha e como corrigir: O teste precisa conhecer a infraestrutura e trocar PostgreSQL exige editar a regra. DI ocorreu, mas a direção de dependência não mudou.
Abordagem correta: Faça checkout possuir um contrato do comportamento que consome; PostgreSQL o satisfaz sem importação inversa.
// package checkout
type OrderSaver interface { Save(Order) error }
type Service struct { orders OrderSaver }
Raciocínio, trade-offs e limites: Comece pelo uso real: o serviço só precisa salvar. Não acrescente Find, Delete ou transações sem uma necessidade do consumidor. A solução é apropriada quando a regra precisa isolar uma borda; um valor simples sem variação pode continuar concreto.
Abra o diagrama relacionado e compare o fluxo.
Pergunta de mercado e entrevista
1. Qual a diferença entre DI e DIP?
Como responder: DI é o mecanismo de fornecer uma dependência. DIP define a direção: a regra depende de um contrato próprio, e o detalhe se adapta a ele. Feche com checkout → OrderSaver ← PostgreSQL.
2. Por que injeção por construtor não basta?
Como responder: Porque receber *postgres.OrderRepository ainda faz checkout conhecer o detalhe. Mostre que trocar o campo para OrderSaver remove essa importação da regra.
3. Qual é o ganho de negócio do DIP?
Como responder: Explique que o ganho é testar decisões sem infraestrutura e trocar detalhes localmente. Não venda interfaces como objetivo: elas são o meio para proteger a regra.
Pratique mais 9 questões de recuperação
Responda sem consultar a aula. O feedback é imediato; volte ao trecho relevante antes de tentar de novo.
DI entrega uma dependência; DIP define:
Em Go, uma interface é satisfeita quando:
Quem deve possuir uma interface pequena?
Onde a implementação concreta é escolhida?
Para testar uma regra sem banco, injete:
Uma interface deve conter:
O detalhe PostgreSQL deve depender de:
Qual sinal pede uma interface?
Após a refatoração, a regra não deve importar: