Fundamentos de Programação/02 - Código que Presta/Semana 06 - Código Limpo e Refatoração8 min
Dependency Inversion Principle
- Quem deve depender de quem: a regra de negócio do banco, ou o banco da regra?
- Qual a diferença entre injeção de dependência e inversão de dependência?
- Por que isso é o que torna teste unitário possível sem mockar o mundo?
- Onde no seu projeto a regra de negócio importa direto o driver do banco?
Conceito
A pergunta é: quem depende de quem?
A regra: detalhes dependem de políticas — nunca o contrário. A regra de negócio não deve conhecer o driver do banco, o cliente HTTP, o formato do arquivo.
Sem inversão, a flecha aponta para fora:
Pedido (regra) -> PostgresRepo -> driver pq
Consequência: trocar de banco toca o domínio. Testar a regra exige um banco. E o domínio compila só se o driver compilar.
Com inversão, o domínio declara o contrato e a infra se adapta:
Pedido (regra) -> interface Repositorio
^
|
PostgresRepo (infra)
A flecha da infra aponta para dentro. O domínio não importa nada.
As duas são confundidas constantemente, e são coisas diferentes:
- Injeção é como a dependência chega: parâmetro de construtor, setter, container.
- Inversão é quem define o contrato: o consumidor ou o fornecedor.
Você pode injetar um tipo concreto — NovoServico(db *sql.DB). Isso é injeção
sem inversão: você ganhou flexibilidade de configuração e continuou acoplado ao
sql.DB. O teste ainda precisa de um banco.
Inversão só acontece quando o parâmetro é uma abstração declarada pelo consumidor.
Por que isso é o que torna teste possível: se o domínio depende de uma interface que
ele mesmo definiu, o teste passa um dublê de três linhas. Se depende de *sql.DB, o teste
precisa de Postgres — e aí você tem teste de integração onde queria teste unitário
(3. Pirâmide de Testes).
O erro mais comum, e o mais difícil de ver: colocar a interface no pacote da infra. Fica assim:
domínio -> importa -> pacote infra (para usar a interface)
A interface existe, o código parece desacoplado, e a flecha não inverteu — o domínio continua importando a infra. A interface tem que morar com quem a consome.
Em Go
Go é possivelmente a linguagem em que DIP é mais natural, por causa da satisfação implícita de interfaces (3. Interfaces):
- o pacote de domínio declara
type Repositorio interface { ... } - o pacote de infra implementa, sem importar o domínio e sem declarar nada
- ninguém precisa conhecer ninguém
Em Java, PostgresRepo implements Repositorio obriga a infra a importar o domínio. Em Go,
nem isso é necessário — a satisfação é estrutural.
E o compilador impõe a arquitetura. O grafo de importação de Go é acíclico por regra (4. Grafos): se você criar dependência circular entre domínio e infra, é erro de compilação, não débito técnico. A camada é verificada pelo build.
Não existe container de DI idiomático em Go. Injeção é passar parâmetro:
func NovoServico(repo Repositorio, log *slog.Logger) *Servico
google/wire existe e resolve por geração de código em tempo de compilação — não por
reflexão em runtime, como Spring. A cultura prefere o main explícito, montando o grafo
de objetos à mão. Isso é verboso e não tem magia.
O tamanho da interface importa muito aqui. A interface do consumidor deve ter só o que o consumidor usa. Se o caso de uso lê um pedido, a interface tem um método — não os quinze que o repositório real oferece. Interface grande anula metade do ganho, porque o dublê fica caro.
context.Context como primeiro parâmetro é o padrão de Go para passar dependência
transversal (cancelamento, prazo, valores de requisição) sem colocá-la em campo de struct.
Respostas às perguntas-guia
1. Quem deve depender de quem: a regra de negócio do banco, ou o banco da regra?
Conceito: o banco da regra. Detalhe depende de política.
Em Go: concretamente — o pacote pedido declara a interface, o pacote postgres
implementa, e postgres importa pedido (para os tipos de domínio), nunca o contrário.
A flecha de importação é a arquitetura.
2. Qual a diferença entre injeção de dependência e inversão de dependência?
Conceito: injeção é como a dependência chega; inversão é quem define o contrato. Injetar tipo concreto é injeção sem inversão.
Em Go: injeção é passar parâmetro (não há container). Inversão é o parâmetro ser uma interface declarada no pacote que a recebe. Se a interface veio do pacote de infra, você tem injeção sem inversão.
3. Por que isso é o que torna teste unitário possível sem mockar o mundo?
Conceito: porque o dublê só precisa satisfazer o contrato mínimo que o consumidor declarou.
Em Go: um dublê é uma struct com os métodos da interface — três linhas, sem biblioteca de mock, sem geração de código (4. Mocks e Stubs). É a satisfação implícita pagando dividendo.
4. Onde no seu projeto a regra de negócio importa direto o driver do banco?
Conceito: procure import de infra em arquivos de domínio.
Em Go, o comando que responde:
grep -rl 'database/sql\|lib/pq\|redis\|mongo' --include='*.go' internal/dominio/
Qualquer resultado é uma violação. E o teste definitivo: o seu pacote de domínio compila sozinho, sem nenhuma dependência externa? Se sim, a inversão está feita.
Trade-offs
Do conceito:
- Inverter compra testabilidade e liberdade de trocar a infra; cobra uma interface a mais e um nível de indireção.
- Inverter tudo produz interfaces com um implementador cada, que é 2. Abstração prematura. Inverta na fronteira de I/O e de dependência externa; não entre dois tipos do seu próprio domínio.
- Interface pequena maximiza o ganho; interface grande anula.
Em Go:
- Satisfação implícita torna a inversão quase gratuita, e permite adiar: crie a interface no dia em que precisar, sem tocar na implementação.
- Sem container de DI, o
mainfica longo e explícito. É verboso e trivial de depurar. - Grafo acíclico imposto pelo compilador dá garantia arquitetural real, ao custo de você não poder "resolver depois" uma dependência circular.
Exemplo prático
package main
import (
"context"
"errors"
"fmt"
)
// ======================================================================
// PACOTE DE DOMÍNIO (imagine: internal/pedido/)
// Não importa banco, nem HTTP, nem nada externo.
// ======================================================================
type Centavos int64
type Pedido struct {
ID int
Total Centavos
Status string
}
var ErrNaoEncontrado = errors.New("pedido não encontrado")
// A interface é declarada AQUI, pelo consumidor, com o MÍNIMO que ele usa.
// O repositório real tem 15 métodos; este caso de uso precisa de 2.
type repositorio interface {
Busca(ctx context.Context, id int) (Pedido, error)
Atualiza(ctx context.Context, p Pedido) error
}
type notificador interface {
Notifica(ctx context.Context, msg string) error
}
// O caso de uso: contém a REGRA, e nada de infra.
type CancelarPedido struct {
repo repositorio
notif notificador
}
func NovoCancelarPedido(r repositorio, n notificador) CancelarPedido {
return CancelarPedido{repo: r, notif: n}
}
func (c CancelarPedido) Executa(ctx context.Context, id int) error {
p, err := c.repo.Busca(ctx, id)
if err != nil {
return fmt.Errorf("cancelar %d: %w", id, err)
}
// A REGRA DE NEGÓCIO, testável sem banco nenhum:
if p.Status == "enviado" {
return fmt.Errorf("cancelar %d: pedido já enviado", id)
}
if p.Status == "cancelado" {
return nil // idempotente
}
p.Status = "cancelado"
if err := c.repo.Atualiza(ctx, p); err != nil {
return fmt.Errorf("cancelar %d: %w", id, err)
}
// falha de notificação não desfaz o cancelamento: decisão de negócio
_ = c.notif.Notifica(ctx, fmt.Sprintf("pedido %d cancelado", id))
return nil
}
// ======================================================================
// PACOTE DE INFRA (imagine: internal/postgres/)
// Implementa a interface do domínio SEM declarar nada — satisfação implícita.
// ======================================================================
type RepoPostgres struct{ dados map[int]Pedido }
func (r *RepoPostgres) Busca(_ context.Context, id int) (Pedido, error) {
p, ok := r.dados[id]
if !ok {
return Pedido{}, ErrNaoEncontrado
}
return p, nil
}
func (r *RepoPostgres) Atualiza(_ context.Context, p Pedido) error {
r.dados[p.ID] = p
return nil
}
// Métodos que o caso de uso NÃO conhece nem precisa
func (r *RepoPostgres) Lista(context.Context) ([]Pedido, error) { return nil, nil }
func (r *RepoPostgres) Remove(context.Context, int) error { return nil }
type EmailNotificador struct{ enviados []string }
func (e *EmailNotificador) Notifica(_ context.Context, msg string) error {
e.enviados = append(e.enviados, msg)
return nil
}
// ======================================================================
// DUBLÊS DE TESTE: três linhas cada, nenhuma biblioteca
// ======================================================================
type repoFalso struct {
p Pedido
errBusca error
atualizados []Pedido
}
func (r *repoFalso) Busca(context.Context, int) (Pedido, error) { return r.p, r.errBusca }
func (r *repoFalso) Atualiza(_ context.Context, p Pedido) error {
r.atualizados = append(r.atualizados, p)
return nil
}
type notifNulo struct{}
func (notifNulo) Notifica(context.Context, string) error { return nil }
func main() {
ctx := context.Background()
// --- produção: main monta o grafo, explicitamente ---
repo := &RepoPostgres{dados: map[int]Pedido{
1: {ID: 1, Total: 10000, Status: "pendente"},
2: {ID: 2, Total: 20000, Status: "enviado"},
}}
mail := &EmailNotificador{}
uc := NovoCancelarPedido(repo, mail)
fmt.Println("cancelar 1:", uc.Executa(ctx, 1))
fmt.Println("cancelar 2:", uc.Executa(ctx, 2))
fmt.Println("cancelar 9:", uc.Executa(ctx, 9))
fmt.Println("status do 1:", repo.dados[1].Status)
fmt.Println("notificações:", mail.enviados)
// --- teste: a MESMA regra, sem banco, sem e-mail, sem rede ---
fmt.Println("\n--- regra testada com dublês ---")
casos := []struct {
nome string
status string
quer string
}{
{"pendente cancela", "pendente", "<nil>"},
{"enviado recusa", "enviado", "cancelar 1: pedido já enviado"},
{"cancelado é idempotente", "cancelado", "<nil>"},
}
for _, c := range casos {
r := &repoFalso{p: Pedido{ID: 1, Status: c.status}}
err := NovoCancelarPedido(r, notifNulo{}).Executa(ctx, 1)
fmt.Printf("%-26s err=%v (atualizou %d vez)\n", c.nome, err, len(r.atualizados))
}
// erro de infra propagado sem o domínio saber o que é infra
r := &repoFalso{errBusca: errors.New("connection refused")}
err := NovoCancelarPedido(r, notifNulo{}).Executa(ctx, 1)
fmt.Println("infra falhou:", err)
}
O teste do meio é o ponto: a regra de cancelamento foi verificada em três cenários sem banco, sem e-mail e sem rede — porque a interface foi declarada por quem consome, com dois métodos. Se ela tivesse os quinze métodos do repositório real, o dublê custaria quinze implementações vazias e ninguém escreveria o teste.
Relacionado
- 3. Interfaces — semana 5, a satisfação implícita é o que torna isso natural em Go
- 2. Single Responsibility Principle — separar por ator exige inverter a dependência de infra
- 4. Mocks e Stubs — semana 7, o dublê que a inversão viabiliza
- 4. Grafos — semana 3, o grafo de importação acíclico imposto pelo compilador
Parte de Semana 06 - Código Limpo e Refatoração · 00 - MOC Fundamentos de Programação