ThiagoSoares
Voltar para o blog
Arquitetura13 min

DIP - Dependency Inversion Principle

Como proteger regras de negócio dos detalhes de infraestrutura usando contratos e composição em Node.js com TypeScript.

Compartilhar artigo

Casos de uso importantes costumam depender de persistência, filas, relógio e comunicação externa. Quando importam esses detalhes diretamente, a política de negócio passa a mudar junto com a infraestrutura.

O Dependency Inversion Principle, ou DIP, reorganiza essa relação. Módulos de alto nível e detalhes de baixo nível dependem de abstrações que representam as necessidades do negócio.

O problema que o princípio resolve

Considere um cadastro que verifica duplicidade, salva o cliente e publica um evento. Se a classe cria um repositório de arquivo e chama uma fila HTTP diretamente, sua regra só funciona com essas escolhas.

Testes precisam tocar disco e rede. Trocar a fila altera o caso de uso. A direção das dependências coloca a política abaixo dos detalhes.

Inversão não é injeção

Injeção de dependência é uma técnica para entregar colaboradores a um objeto. DIP é o princípio que decide a direção entre políticas e detalhes.

Receber uma classe concreta pelo construtor melhora a criação, mas não inverte a relação se o caso de uso continuar tipado por aquela infraestrutura. A abstração deve ser definida a partir do que a regra precisa.

Exemplo ruim

code
import { appendFile, readFile } from "node:fs/promises";
 
type Customer = {
  id: string;
  email: string;
  createdAt: string;
};
 
class RegisterCustomer {
  async execute(id: string, email: string): Promise<Customer> {
    const filePath = "data/customers.json";
    const content = await readFile(filePath, "utf8").catch(() => "[]");
    const customers = JSON.parse(content) as Customer[];
 
    if (customers.some((customer) => customer.email === email)) {
      throw new Error("E-mail já cadastrado");
    }
 
    const customer = {
      id,
      email,
      createdAt: new Date().toISOString()
    };
 
    await appendFile(filePath, `${JSON.stringify(customer)}\n`, "utf8");
    await fetch("https://queue.internal/events", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ type: "customer.registered", customer })
    });
 
    return customer;
  }
}

Por que o exemplo é ruim?

A regra conhece formato de arquivo, relógio global, serialização e endpoint da fila. Além do acoplamento, há uma inconsistência: a leitura espera um array JSON, mas a escrita adiciona linhas independentes.

Validar duplicidade exige preparar arquivo. Simular o tempo requer alterar recursos globais. Uma indisponibilidade da fila ocorre depois da persistência sem uma política explícita.

Refatoração

Definimos portas com o vocabulário do caso de uso:

code
interface CustomerRepository {
  findByEmail(email: string): Promise<Customer | null>;
  save(customer: Customer): Promise<void>;
}
 
interface EventPublisher {
  publish(event: {
    type: "customer.registered";
    customerId: string;
    occurredAt: string;
  }): Promise<void>;
}
 
interface Clock {
  now(): Date;
}

Esses contratos não mencionam arquivos, HTTP ou bibliotecas. Eles expressam operações necessárias à política.

Exemplo bom

code
class RegisterCustomer {
  constructor(
    private readonly customers: CustomerRepository,
    private readonly events: EventPublisher,
    private readonly clock: Clock
  ) {}
 
  async execute(id: string, email: string): Promise<Customer> {
    const normalizedEmail = email.trim().toLowerCase();
    const existingCustomer = await this.customers.findByEmail(normalizedEmail);
 
    if (existingCustomer) {
      throw new Error("E-mail já cadastrado");
    }
 
    const customer = {
      id,
      email: normalizedEmail,
      createdAt: this.clock.now().toISOString()
    };
 
    await this.customers.save(customer);
    await this.events.publish({
      type: "customer.registered",
      customerId: customer.id,
      occurredAt: customer.createdAt
    });
 
    return customer;
  }
}

O caso de uso continua dono da ordem e das regras. Repositório, publicador e relógio oferecem detalhes por trás dos contratos.

Detalhes implementam as portas

Uma implementação em memória é útil para testes e desenvolvimento:

code
class MemoryCustomerRepository implements CustomerRepository {
  private readonly customers = new Map<string, Customer>();
 
  async findByEmail(email: string): Promise<Customer | null> {
    return this.customers.get(email) ?? null;
  }
 
  async save(customer: Customer): Promise<void> {
    this.customers.set(customer.email, customer);
  }
}
 
class SystemClock implements Clock {
  now(): Date {
    return new Date();
  }
}

Uma implementação com banco ou fila dependeria das APIs escolhidas, mas o caso de uso não precisaria importá-las.

A composição fica na borda

Algum lugar ainda precisa conhecer classes concretas. Esse ponto é a composição da aplicação:

code
const customers = new MemoryCustomerRepository();
const events: EventPublisher = {
  async publish(event) {
    console.log(JSON.stringify(event));
  }
};
 
const registerCustomer = new RegisterCustomer(
  customers,
  events,
  new SystemClock()
);

DIP não elimina dependências; ele as move. O núcleo conhece abstrações estáveis, enquanto a borda conecta detalhes concretos.

Testando a política

Um relógio controlado e um publicador em memória tornam o comportamento determinístico sem alterar globais:

code
const fixedClock: Clock = {
  now: () => new Date("2027-03-25T12:00:00.000Z")
};
 
const publishedEvents: Array<{ type: string; customerId: string }> = [];
const events: EventPublisher = {
  async publish(event) {
    publishedEvents.push(event);
  }
};

O teste pode verificar normalização, duplicidade, data e evento como regras do caso de uso. Infraestrutura real recebe seus próprios testes de integração.

Benefícios

  • regras de negócio independentes de infraestrutura;
  • testes rápidos e determinísticos;
  • troca de detalhes com impacto local;
  • necessidades do domínio explícitas em contratos;
  • composição centralizada;
  • fronteira clara entre política e mecanismo.

Quando aplicar?

Sempre pergunte: "As regras de alto nível dependem diretamente de detalhes de baixo nível que dificultam testes ou mudanças?" Se a resposta for "sim", é hora de aplicar o DIP.

Fechamento

Dependency Inversion Principle mantém políticas importantes acima dos mecanismos que as executam. O núcleo define do que precisa, e a infraestrutura adapta suas ferramentas a esse contrato.

Aplicado nas fronteiras certas, o princípio reduz o custo de testar e substituir detalhes. Aplicado em toda função, apenas troca acoplamento por cerimônia.

Série SOLID

Este artigo faz parte de uma série de artigos: