trilha

Fundamentos de Programação/02 - Código que Presta/Semana 06 - Código Limpo e Refatoração8 min

Dependency Inversion Principle

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

Injeção de dependência ≠ inversão de dependência

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 concretoNovoServico(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 main fica 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


Parte de Semana 06 - Código Limpo e Refatoração · 00 - MOC Fundamentos de Programação

Buscar

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